go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/data/convert_to.go (about) 1 // Copyright 2023 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package data holds data manipulation functions. 16 // 17 // Most functionality should be put into a subpackage of data, but it's 18 // acceptable to have small, common, functions here which don't fit into 19 // a subpackage. 20 package data 21 22 import ( 23 "reflect" 24 ) 25 26 // LosslessConvertTo will attempt to convert a go value into a given type losslessly. 27 // 28 // We only want to perform conversions which are ALWAYS lossless, which means: 29 // - untyped nil -> interface, pointer, slice 30 // - intX to intY where X <= Y 31 // - uintX to uintY where X <= Y 32 // - uintX to intY where X < Y 33 // - floatX to floatY where X <= Y 34 // - complexX to complexY where X <= Y 35 // - string to (string, []byte, []rune) 36 // - all other conversions implemented by reflect.Value.Convert. 37 // 38 // Other conversions to string allowed by reflect.Type.ConvertibleTo are 39 // disallowed, because it allows e.g. int->string, but in the sense of 40 // string(rune(1938)), which is almost certainly NOT what you want. 41 // ConvertibleTo also allows lossy conversions (e.g. int64->int8). 42 // 43 // If you arrive here because Assert(t, something, Comparison[else]) worked in 44 // a surprising way, you probably need to add more conditions to this function. 45 // 46 // Note the "ALWAYS lossless" condition is important - we do not want to allow 47 // int64(100) -> int8 even though this hasn't lost data. This is because if the 48 // values change, this function will start returning ok=false. We want this 49 // conversion to ALWAYS fail with a type conversion error, requiring the caller 50 // author to change the input/output types explicitly. 51 func LosslessConvertTo[T any](value any) (T, bool) { 52 var ret T 53 // need `to` to be assignable, so & and Elem(). 54 to := reflect.ValueOf(&ret).Elem() 55 56 if value == nil { 57 switch to.Kind() { 58 case reflect.Interface, reflect.Pointer, reflect.Chan, reflect.Slice, 59 reflect.Map, reflect.Func: 60 // just keep `to` as the default value 61 return ret, true 62 } 63 // else: nil -> * is not allowed 64 return ret, false 65 } 66 67 from := reflect.ValueOf(value) 68 fromT, toT := from.Type(), to.Type() 69 fromK, toK := fromT.Kind(), toT.Kind() 70 71 switch { 72 case toK == reflect.Interface: 73 ok := fromT.ConvertibleTo(toT) 74 if ok { 75 to.Set(from.Convert(toT)) 76 } 77 return ret, ok 78 79 case fromK >= reflect.Int && fromK <= reflect.Int64: 80 if toK >= reflect.Int && toK <= reflect.Int64 && toT.Bits() >= fromT.Bits() { 81 to.SetInt(from.Int()) 82 return ret, true 83 } 84 85 case fromK >= reflect.Uint && fromK <= reflect.Uint64: 86 switch { 87 case toK >= reflect.Uint && toK <= reflect.Uint64 && toT.Bits() >= fromT.Bits(): 88 to.SetUint(from.Uint()) 89 return ret, true 90 case toK >= reflect.Int && toK <= reflect.Int64 && toT.Bits() > fromT.Bits(): 91 to.SetInt(int64(from.Uint())) 92 return ret, true 93 } 94 95 case fromK >= reflect.Float32 && fromK <= reflect.Float64: 96 if toK >= reflect.Float32 && toK <= reflect.Float64 && toT.Bits() >= fromT.Bits() { 97 to.SetFloat(from.Float()) 98 return ret, true 99 } 100 101 case fromK >= reflect.Complex64 && fromK <= reflect.Complex128: 102 if toK >= reflect.Complex64 && toK <= reflect.Complex128 && toT.Bits() >= fromT.Bits() { 103 to.SetComplex(from.Complex()) 104 return ret, true 105 } 106 107 case fromK == reflect.String: 108 switch { 109 case toK == reflect.String: 110 to.SetString(from.String()) 111 return ret, true 112 113 case toK == reflect.Slice && toT.Elem().Kind() == reflect.Uint8: 114 // []byte 115 to.SetBytes([]byte(from.String())) 116 return ret, true 117 118 case toK == reflect.Slice && toT.Elem().Kind() == reflect.Int32: 119 // []rune - we use Convert because reflect has a fancy internal 120 // implementation for setRunes that we can't use :/ 121 to.Set(from.Convert(toT)) 122 return ret, true 123 } 124 125 case fromT.ConvertibleTo(toT): 126 // We rely on the default conversion rules for all other target types. 127 to.Set(from.Convert(toT)) 128 return ret, true 129 130 } 131 132 return ret, false 133 }