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  }