Ctrl K
ComunidadGuías…

internal/rest/nonnil.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}