blob: 22c90e5984ad5b1ef697dadf4421e0cf7100114e [file] [log] [blame]
// Copyright 2023 The LUCI 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 data holds data manipulation functions.
//
// Most functionality should be put into a subpackage of data, but it's
// acceptable to have small, common, functions here which don't fit into
// a subpackage.
package data
import (
"reflect"
)
// LosslessConvertTo will attempt to convert a go value into a given type losslessly.
//
// We only want to perform conversions which are ALWAYS lossless, which means:
// - untyped nil -> interface, pointer, slice
// - intX to intY where X <= Y
// - uintX to uintY where X <= Y
// - uintX to intY where X < Y
// - floatX to floatY where X <= Y
// - complexX to complexY where X <= Y
// - string to (string, []byte, []rune)
// - all other conversions implemented by reflect.Value.Convert.
//
// Other conversions to string allowed by reflect.Type.ConvertibleTo are
// disallowed, because it allows e.g. int->string, but in the sense of
// string(rune(1938)), which is almost certainly NOT what you want.
// ConvertibleTo also allows lossy conversions (e.g. int64->int8).
//
// If you arrive here because Assert(t, something, Comparison[else]) worked in
// a surprising way, you probably need to add more conditions to this function.
//
// Note the "ALWAYS lossless" condition is important - we do not want to allow
// int64(100) -> int8 even though this hasn't lost data. This is because if the
// values change, this function will start returning ok=false. We want this
// conversion to ALWAYS fail with a type conversion error, requiring the caller
// author to change the input/output types explicitly.
func LosslessConvertTo[T any](value any) (T, bool) {
var ret T
// need `to` to be assignable, so & and Elem().
to := reflect.ValueOf(&ret).Elem()
if value == nil {
switch to.Kind() {
case reflect.Interface, reflect.Pointer, reflect.Chan, reflect.Slice,
reflect.Map, reflect.Func:
// just keep `to` as the default value
return ret, true
}
// else: nil -> * is not allowed
return ret, false
}
from := reflect.ValueOf(value)
fromT, toT := from.Type(), to.Type()
fromK, toK := fromT.Kind(), toT.Kind()
switch {
case toK == reflect.Interface:
ok := fromT.ConvertibleTo(toT)
if ok {
to.Set(from.Convert(toT))
}
return ret, ok
case fromK >= reflect.Int && fromK <= reflect.Int64:
if toK >= reflect.Int && toK <= reflect.Int64 && toT.Bits() >= fromT.Bits() {
to.SetInt(from.Int())
return ret, true
}
case fromK >= reflect.Uint && fromK <= reflect.Uint64:
switch {
case toK >= reflect.Uint && toK <= reflect.Uint64 && toT.Bits() >= fromT.Bits():
to.SetUint(from.Uint())
return ret, true
case toK >= reflect.Int && toK <= reflect.Int64 && toT.Bits() > fromT.Bits():
to.SetInt(int64(from.Uint()))
return ret, true
}
case fromK >= reflect.Float32 && fromK <= reflect.Float64:
if toK >= reflect.Float32 && toK <= reflect.Float64 && toT.Bits() >= fromT.Bits() {
to.SetFloat(from.Float())
return ret, true
}
case fromK >= reflect.Complex64 && fromK <= reflect.Complex128:
if toK >= reflect.Complex64 && toK <= reflect.Complex128 && toT.Bits() >= fromT.Bits() {
to.SetComplex(from.Complex())
return ret, true
}
case fromK == reflect.String:
switch {
case toK == reflect.String:
to.SetString(from.String())
return ret, true
case toK == reflect.Slice && toT.Elem().Kind() == reflect.Uint8:
// []byte
to.SetBytes([]byte(from.String()))
return ret, true
case toK == reflect.Slice && toT.Elem().Kind() == reflect.Int32:
// []rune - we use Convert because reflect has a fancy internal
// implementation for setRunes that we can't use :/
to.Set(from.Convert(toT))
return ret, true
}
case fromT.ConvertibleTo(toT):
// We rely on the default conversion rules for all other target types.
to.Set(from.Convert(toT))
return ret, true
}
return ret, false
}