diff --git a/tpl/debug/debug.go b/tpl/debug/debug.go index ae1acf5eb..120f9b773 100644 --- a/tpl/debug/debug.go +++ b/tpl/debug/debug.go @@ -15,6 +15,10 @@ package debug import ( + "reflect" + "sort" + + "github.com/sanity-io/litter" "encoding/json" "sort" "sync" @@ -181,3 +185,47 @@ func (ns *Namespace) TestDeprecationErr(item, alternative string) string { hugo.Deprecate(item, alternative, v.String()) return "" } + +// List returns a slice of field names and method names of the struct/pointer or keys of the map +// This method scans the provided value shallow, non-recursively. +func (ns *Namespace) List(val any) []string { + + fields := make([]string, 0) + value := reflect.ValueOf(val) + + if value.Kind() == reflect.Map { + for _, key := range value.MapKeys() { + fields = append(fields, key.String()) + sort.Strings(fields) + } + } + + // Dereference the pointer if needed + if value.Kind() == reflect.Pointer { + value = value.Elem() + } + + if value.Kind() == reflect.Struct { + // Iterate over the fields + for i := 0; i < value.NumField(); i++ { + field := value.Type().Field(i) + + // Only add exported fields + if field.PkgPath == "" { + fields = append(fields, field.Name) + } + } + + // Calling NumMethod() on the pointer type returns the number of methods + // defined for the pointer type as well as the non pointer type. + // Calling NumMethod() on the non pointer type returns on the other hand only the number of non pointer methods. + pointerType := reflect.PointerTo(value.Type()) + + for i := 0; i < pointerType.NumMethod(); i++ { + method := pointerType.Method(i) + fields = append(fields, method.Name) + } + } + + return fields +} diff --git a/tpl/debug/debug_test.go b/tpl/debug/debug_test.go new file mode 100644 index 000000000..133db11bf --- /dev/null +++ b/tpl/debug/debug_test.go @@ -0,0 +1,61 @@ +// Copyright 2023 The Hugo Authors. All rights reserved. +// +// 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 debug + +import ( + "fmt" + "reflect" + "testing" +) + +type User struct { + Name string + Address any + foo string +} + +func (u *User) M1() string { return "" } +func (u *User) M2(v string) string { return "" } +func (u *User) m3(v string) string { return "" } + +// Non Pointer type methods +func (u User) M4(v string) string { return "" } +func (u User) m5(v string) string { return "" } + +func TestList(t *testing.T) { + t.Parallel() + + namespace := new(Namespace) + + for i, test := range []struct { + val any + expect []string + }{ + // Map + {map[string]any{"key1": 1, "key2": 2, "key3": 3}, []string{"key1", "key2", "key3"}}, + // Map non string keys + {map[int]any{1: 1, 2: 2, 3: 3}, []string{"", "", ""}}, + // Struct + {User{}, []string{"Name", "Address", "M1", "M2", "M4"}}, + // Pointer + {&User{}, []string{"Name", "Address", "M1", "M2", "M4"}}, + } { + t.Run(fmt.Sprintf("test%d", i), func(t *testing.T) { + result := namespace.List(test.val) + + if !reflect.DeepEqual(result, test.expect) { + t.Fatalf("List called with value: %#v got\n%#v but expected\n%#v", test.val, result, test.expect) + } + }) + } +}