github.com/neohugo/neohugo@v0.123.8/common/hreflect/helpers.go (about) 1 // Copyright 2024 The Hugo Authors. All rights reserved. 2 // Some functions in this file (see comments) is based on the Go source code, 3 // copyright The Go Authors and governed by a BSD-style license. 4 // 5 // Licensed under the Apache License, Version 2.0 (the "License"); 6 // you may not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 16 // Package hreflect contains reflect helpers. 17 package hreflect 18 19 import ( 20 "context" 21 "reflect" 22 "sync" 23 "time" 24 25 "github.com/neohugo/neohugo/common/htime" 26 "github.com/neohugo/neohugo/common/maps" 27 "github.com/neohugo/neohugo/common/types" 28 ) 29 30 // TODO(bep) replace the private versions in /tpl with these. 31 // IsNumber returns whether the given kind is a number. 32 func IsNumber(kind reflect.Kind) bool { 33 return IsInt(kind) || IsUint(kind) || IsFloat(kind) 34 } 35 36 // IsInt returns whether the given kind is an int. 37 func IsInt(kind reflect.Kind) bool { 38 switch kind { 39 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 40 return true 41 default: 42 return false 43 } 44 } 45 46 // IsUint returns whether the given kind is an uint. 47 func IsUint(kind reflect.Kind) bool { 48 switch kind { 49 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 50 return true 51 default: 52 return false 53 } 54 } 55 56 // IsFloat returns whether the given kind is a float. 57 func IsFloat(kind reflect.Kind) bool { 58 switch kind { 59 case reflect.Float32, reflect.Float64: 60 return true 61 default: 62 return false 63 } 64 } 65 66 // IsTruthful returns whether in represents a truthful value. 67 // See IsTruthfulValue 68 func IsTruthful(in any) bool { 69 switch v := in.(type) { 70 case reflect.Value: 71 return IsTruthfulValue(v) 72 default: 73 return IsTruthfulValue(reflect.ValueOf(in)) 74 } 75 } 76 77 var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem() 78 79 // IsTruthfulValue returns whether the given value has a meaningful truth value. 80 // This is based on template.IsTrue in Go's stdlib, but also considers 81 // IsZero and any interface value will be unwrapped before it's considered 82 // for truthfulness. 83 // 84 // Based on: 85 // https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L306 86 func IsTruthfulValue(val reflect.Value) (truth bool) { 87 val = indirectInterface(val) 88 89 if !val.IsValid() { 90 // Something like var x interface{}, never set. It's a form of nil. 91 return 92 } 93 94 if val.Type().Implements(zeroType) { 95 return !val.Interface().(types.Zeroer).IsZero() 96 } 97 98 switch val.Kind() { 99 case reflect.Array, reflect.Map, reflect.Slice, reflect.String: 100 truth = val.Len() > 0 101 case reflect.Bool: 102 truth = val.Bool() 103 case reflect.Complex64, reflect.Complex128: 104 truth = val.Complex() != 0 105 case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface: 106 truth = !val.IsNil() 107 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 108 truth = val.Int() != 0 109 case reflect.Float32, reflect.Float64: 110 truth = val.Float() != 0 111 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 112 truth = val.Uint() != 0 113 case reflect.Struct: 114 truth = true // Struct values are always true. 115 default: 116 return 117 } 118 119 return 120 } 121 122 type methodKey struct { 123 typ reflect.Type 124 name string 125 } 126 127 type methods struct { 128 sync.RWMutex 129 cache map[methodKey]int 130 } 131 132 var methodCache = &methods{cache: make(map[methodKey]int)} 133 134 // GetMethodByName is the same as reflect.Value.MethodByName, but it caches the 135 // type lookup. 136 func GetMethodByName(v reflect.Value, name string) reflect.Value { 137 index := GetMethodIndexByName(v.Type(), name) 138 139 if index == -1 { 140 return reflect.Value{} 141 } 142 143 return v.Method(index) 144 } 145 146 // GetMethodIndexByName returns the index of the method with the given name, or 147 // -1 if no such method exists. 148 func GetMethodIndexByName(tp reflect.Type, name string) int { 149 k := methodKey{tp, name} 150 methodCache.RLock() 151 index, found := methodCache.cache[k] 152 methodCache.RUnlock() 153 if found { 154 return index 155 } 156 157 methodCache.Lock() 158 defer methodCache.Unlock() 159 160 m, ok := tp.MethodByName(name) 161 index = m.Index 162 if !ok { 163 index = -1 164 } 165 methodCache.cache[k] = index 166 167 if !ok { 168 return -1 169 } 170 171 return m.Index 172 } 173 174 var ( 175 timeType = reflect.TypeOf((*time.Time)(nil)).Elem() 176 asTimeProviderType = reflect.TypeOf((*htime.AsTimeProvider)(nil)).Elem() 177 ) 178 179 // IsTime returns whether tp is a time.Time type or if it can be converted into one 180 // in ToTime. 181 func IsTime(tp reflect.Type) bool { 182 if tp == timeType { 183 return true 184 } 185 186 if tp.Implements(asTimeProviderType) { 187 return true 188 } 189 return false 190 } 191 192 // IsValid returns whether v is not nil and a valid value. 193 func IsValid(v reflect.Value) bool { 194 if !v.IsValid() { 195 return false 196 } 197 198 switch v.Kind() { 199 case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: 200 return !v.IsNil() 201 } 202 203 return true 204 } 205 206 // AsTime returns v as a time.Time if possible. 207 // The given location is only used if the value implements AsTimeProvider (e.g. go-toml local). 208 // A zero Time and false is returned if this isn't possible. 209 // Note that this function does not accept string dates. 210 func AsTime(v reflect.Value, loc *time.Location) (time.Time, bool) { 211 if v.Kind() == reflect.Interface { 212 return AsTime(v.Elem(), loc) 213 } 214 215 if v.Type() == timeType { 216 return v.Interface().(time.Time), true 217 } 218 219 if v.Type().Implements(asTimeProviderType) { 220 return v.Interface().(htime.AsTimeProvider).AsTime(loc), true 221 } 222 223 return time.Time{}, false 224 } 225 226 func CallMethodByName(cxt context.Context, name string, v reflect.Value) []reflect.Value { 227 fn := v.MethodByName(name) 228 var args []reflect.Value 229 tp := fn.Type() 230 if tp.NumIn() > 0 { 231 if tp.NumIn() > 1 { 232 panic("not supported") 233 } 234 first := tp.In(0) 235 if IsContextType(first) { 236 args = append(args, reflect.ValueOf(cxt)) 237 } 238 } 239 240 return fn.Call(args) 241 } 242 243 // Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931 244 func indirectInterface(v reflect.Value) reflect.Value { 245 if v.Kind() != reflect.Interface { 246 return v 247 } 248 if v.IsNil() { 249 return reflect.Value{} 250 } 251 return v.Elem() 252 } 253 254 var contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem() 255 256 var isContextCache = maps.NewCache[reflect.Type, bool]() 257 258 type k string 259 260 var contextTypeValue = reflect.TypeOf(context.WithValue(context.Background(), k("key"), 32)) 261 262 // IsContextType returns whether tp is a context.Context type. 263 func IsContextType(tp reflect.Type) bool { 264 if tp == contextTypeValue { 265 return true 266 } 267 if tp == contextInterface { 268 return true 269 } 270 271 return isContextCache.GetOrCreate(tp, func() bool { 272 return tp.Implements(contextInterface) 273 }) 274 }