internal/rest/nonnil.go
Rama main · réplica de lectura del repositorio original.
package rest
import (
"reflect"
"sync"
"github.com/danielgtaylor/huma/v2"
)
// Arrays in the API are never null: the spec types them as plain arrays
// (huma.DefaultArrayNullable is off), so a request sending null is rejected
// and a response must send [] instead. Handlers often build output from nil
// slices, so emptySlices rewrites every nil slice in a response body to an
// empty one rather than relying on each handler to remember.
func init() {
huma.DefaultArrayNullable = false
}
// emptySlices is a huma transformer that returns v with every nil slice,
// at any depth, replaced by an empty slice of the same type.
func emptySlices(_ huma.Context, _ string, v any) (any, error) {
if v == nil {
return v, nil
}
rv := reflect.ValueOf(v)
if !typeHasSlices(rv.Type()) {
return v, nil
}
return fillSlices(rv).Interface(), nil
}
var slicesByType sync.Map // reflect.Type -> bool
// typeHasSlices caches hasSlices per type.
func typeHasSlices(t reflect.Type) bool {
if v, ok := slicesByType.Load(t); ok {
return v.(bool)
}
has := hasSlices(t, map[reflect.Type]bool{})
slicesByType.Store(t, has)
return has
}
// hasSlices reports whether values of t can contain a slice, so values
// without any skip the copy. Interface values (event payloads and other
// decoded JSON) are left alone: JSON decoding never produces a nil slice.
func hasSlices(t reflect.Type, seen map[reflect.Type]bool) bool {
if seen[t] {
return false
}
seen[t] = true
switch t.Kind() {
case reflect.Slice:
return t.Elem().Kind() != reflect.Uint8 // []byte marshals as a string
case reflect.Pointer, reflect.Array:
return hasSlices(t.Elem(), seen)
case reflect.Map:
return hasSlices(t.Elem(), seen)
case reflect.Struct:
for i := range t.NumField() {
if t.Field(i).IsExported() && hasSlices(t.Field(i).Type, seen) {
return true
}
}
}
return false
}
// fillSlices returns a copy of v with nil slices replaced by empty ones.
// It copies rather than mutates, so handler-owned values stay untouched.
func fillSlices(v reflect.Value) reflect.Value {
if !typeHasSlices(v.Type()) {
return v
}
switch v.Kind() {
case reflect.Slice:
if v.Type().Elem().Kind() == reflect.Uint8 {
return v
}
if v.IsNil() {
return reflect.MakeSlice(v.Type(), 0, 0)
}
out := reflect.MakeSlice(v.Type(), v.Len(), v.Len())
for i := range v.Len() {
out.Index(i).Set(fillSlices(v.Index(i)))
}
return out
case reflect.Pointer:
if v.IsNil() {
return v
}
out := reflect.New(v.Type().Elem())
out.Elem().Set(fillSlices(v.Elem()))
return out
case reflect.Struct:
out := reflect.New(v.Type()).Elem()
out.Set(v)
for i := range v.NumField() {
if !v.Type().Field(i).IsExported() {
continue
}
out.Field(i).Set(fillSlices(v.Field(i)))
}
return out
case reflect.Map:
if v.IsNil() {
return v
}
out := reflect.MakeMapWithSize(v.Type(), v.Len())
iter := v.MapRange()
for iter.Next() {
out.SetMapIndex(iter.Key(), fillSlices(iter.Value()))
}
return out
}
return v
}