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 }