go.temporal.io/server@v1.23.0/common/searchattribute/stringify.go (about)

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package searchattribute
    26  
    27  import (
    28  	"encoding/json"
    29  	"fmt"
    30  	"reflect"
    31  	"strconv"
    32  	"strings"
    33  	"time"
    34  
    35  	commonpb "go.temporal.io/api/common/v1"
    36  	enumspb "go.temporal.io/api/enums/v1"
    37  
    38  	"go.temporal.io/server/common/payload"
    39  )
    40  
    41  // Stringify converts search attributes to map of strings using (in order):
    42  // 1. type from MetadataType field,
    43  // 2. type from typeMap (can be nil).
    44  // In case of error, it will continue to next search attribute and return last error.
    45  // Single values are converted using strconv, arrays are converted using json.Marshal.
    46  // Search attributes with `nil` values are skipped.
    47  func Stringify(searchAttributes *commonpb.SearchAttributes, typeMap *NameTypeMap) (map[string]string, error) {
    48  	if len(searchAttributes.GetIndexedFields()) == 0 {
    49  		return nil, nil
    50  	}
    51  
    52  	result := make(map[string]string, len(searchAttributes.GetIndexedFields()))
    53  	var lastErr error
    54  
    55  	for saName, saPayload := range searchAttributes.GetIndexedFields() {
    56  		saType := enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED
    57  		if typeMap != nil {
    58  			saType, _ = typeMap.getType(saName, customCategory|predefinedCategory)
    59  		}
    60  		saValue, err := DecodeValue(saPayload, saType, true)
    61  		if err != nil {
    62  			// If DecodeValue failed, save error and use raw JSON from Data field.
    63  			result[saName] = string(saPayload.GetData())
    64  			lastErr = err
    65  			continue
    66  		}
    67  
    68  		if saValue == nil {
    69  			continue
    70  		}
    71  
    72  		switch saTypedValue := saValue.(type) {
    73  		case string:
    74  			result[saName] = saTypedValue
    75  		case int64:
    76  			result[saName] = strconv.FormatInt(saTypedValue, 10)
    77  		case float64:
    78  			result[saName] = strconv.FormatFloat(saTypedValue, 'f', -1, 64)
    79  		case bool:
    80  			result[saName] = strconv.FormatBool(saTypedValue)
    81  		case time.Time:
    82  			result[saName] = saTypedValue.Format(time.RFC3339Nano)
    83  		default:
    84  			switch reflect.TypeOf(saValue).Kind() {
    85  			case reflect.Slice, reflect.Array:
    86  				valBytes, err := json.Marshal(saValue)
    87  				if err != nil {
    88  					result[saName] = string(saPayload.GetData())
    89  					lastErr = err
    90  					continue
    91  				}
    92  				result[saName] = string(valBytes)
    93  			default:
    94  				result[saName] = fmt.Sprintf("%v", saTypedValue)
    95  			}
    96  		}
    97  	}
    98  
    99  	return result, lastErr
   100  }
   101  
   102  // Parse converts maps of search attribute strings to search attributes.
   103  // typeMap can be nil (values will be parsed with strconv and MetadataType field won't be set).
   104  // In case of error, it will continue to next search attribute and return last error.
   105  // Single values are parsed using strconv, arrays are parsed using json.Unmarshal.
   106  func Parse(searchAttributesStr map[string]string, typeMap *NameTypeMap) (*commonpb.SearchAttributes, error) {
   107  	if len(searchAttributesStr) == 0 {
   108  		return nil, nil
   109  	}
   110  
   111  	searchAttributes := &commonpb.SearchAttributes{
   112  		IndexedFields: make(map[string]*commonpb.Payload, len(searchAttributesStr)),
   113  	}
   114  	var lastErr error
   115  
   116  	for saName, saValStr := range searchAttributesStr {
   117  		saType := enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED
   118  		if typeMap != nil {
   119  			saType, _ = typeMap.getType(saName, customCategory|predefinedCategory)
   120  		}
   121  		saValPayload, err := parseValueOrArray(saValStr, saType)
   122  		if err != nil {
   123  			lastErr = err
   124  		}
   125  		searchAttributes.IndexedFields[saName] = saValPayload
   126  	}
   127  
   128  	return searchAttributes, lastErr
   129  }
   130  
   131  func parseValueOrArray(valStr string, t enumspb.IndexedValueType) (*commonpb.Payload, error) {
   132  	var val interface{}
   133  
   134  	if isJsonArray(valStr) {
   135  		var err error
   136  		val, err = parseJsonArray(valStr, t)
   137  		if err != nil {
   138  			return nil, err
   139  		}
   140  	} else {
   141  		var err error
   142  		val, err = parseValueTyped(valStr, t)
   143  		if err != nil {
   144  			return nil, err
   145  		}
   146  	}
   147  
   148  	valPayload, err := payload.Encode(val)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  
   153  	setMetadataType(valPayload, t)
   154  	return valPayload, nil
   155  }
   156  
   157  func parseValueTyped(valStr string, t enumspb.IndexedValueType) (interface{}, error) {
   158  	var val interface{}
   159  	var err error
   160  
   161  	switch t {
   162  	case enumspb.INDEXED_VALUE_TYPE_TEXT,
   163  		enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   164  		enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST:
   165  		val = valStr
   166  	case enumspb.INDEXED_VALUE_TYPE_INT:
   167  		val, err = strconv.ParseInt(valStr, 10, 64)
   168  	case enumspb.INDEXED_VALUE_TYPE_DOUBLE:
   169  		val, err = strconv.ParseFloat(valStr, 64)
   170  	case enumspb.INDEXED_VALUE_TYPE_BOOL:
   171  		val, err = strconv.ParseBool(valStr)
   172  	case enumspb.INDEXED_VALUE_TYPE_DATETIME:
   173  		val, err = time.Parse(time.RFC3339Nano, valStr)
   174  	case enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED:
   175  		val = parseValueUnspecified(valStr)
   176  	default:
   177  		err = fmt.Errorf("%w: %v", ErrInvalidType, t)
   178  	}
   179  
   180  	return val, err
   181  }
   182  
   183  func parseValueUnspecified(valStr string) interface{} {
   184  	var val interface{}
   185  	var err error
   186  
   187  	if val, err = strconv.ParseInt(valStr, 10, 64); err == nil {
   188  	} else if val, err = strconv.ParseBool(valStr); err == nil {
   189  	} else if val, err = strconv.ParseFloat(valStr, 64); err == nil {
   190  	} else if val, err = time.Parse(time.RFC3339Nano, valStr); err == nil {
   191  	} else if isJsonArray(valStr) {
   192  		arr, err := parseJsonArray(valStr, enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED)
   193  		if err != nil {
   194  			val = valStr
   195  		} else {
   196  			val = arr
   197  		}
   198  	} else {
   199  		val = valStr
   200  	}
   201  
   202  	return val
   203  }
   204  
   205  func isJsonArray(str string) bool {
   206  	str = strings.TrimSpace(str)
   207  	return strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]")
   208  }
   209  
   210  func parseJsonArray(str string, t enumspb.IndexedValueType) (interface{}, error) {
   211  	switch t {
   212  	case enumspb.INDEXED_VALUE_TYPE_TEXT,
   213  		enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   214  		enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST:
   215  		var result []string
   216  		err := json.Unmarshal([]byte(str), &result)
   217  		return result, err
   218  	case enumspb.INDEXED_VALUE_TYPE_INT:
   219  		var result []int64
   220  		err := json.Unmarshal([]byte(str), &result)
   221  		return result, err
   222  	case enumspb.INDEXED_VALUE_TYPE_DOUBLE:
   223  		var result []float64
   224  		err := json.Unmarshal([]byte(str), &result)
   225  		return result, err
   226  	case enumspb.INDEXED_VALUE_TYPE_BOOL:
   227  		var result []bool
   228  		err := json.Unmarshal([]byte(str), &result)
   229  		return result, err
   230  	case enumspb.INDEXED_VALUE_TYPE_DATETIME:
   231  		var result []time.Time
   232  		err := json.Unmarshal([]byte(str), &result)
   233  		return result, err
   234  	case enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED:
   235  		var result []interface{}
   236  		err := json.Unmarshal([]byte(str), &result)
   237  		return result, err
   238  	default:
   239  		return nil, fmt.Errorf("%w: %v", ErrInvalidType, t)
   240  	}
   241  }