Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(debug): add List function to retrieve struct field names and map…
… keys
  • Loading branch information
UrielOfir committed Sep 13, 2025
commit 03928f3d5792a8068d59cb73c1abdbe4612f71b6
61 changes: 61 additions & 0 deletions tpl/debug/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package debug

import (
"encoding/json"
"reflect"
"sort"
"sync"
"time"
Expand Down Expand Up @@ -120,6 +121,66 @@ func (ns *Namespace) VisualizeSpaces(val any) string {
return string(util.VisualizeSpaces([]byte(s)))
}

// List returns a string slice of field names and methods for structs/pointers,
// or keys for maps. This function uses reflection and is non-recursive.
// For structs and pointers to structs, it returns all exported field names and method names.
// For maps, it returns all keys converted to strings.
// For other types, it returns an empty slice.
func (ns *Namespace) List(val any) []string {
if val == nil {
return []string{}
}

v := reflect.ValueOf(val)
t := v.Type()

// Handle pointers by dereferencing them
if t.Kind() == reflect.Ptr {
if v.IsNil() {
return []string{}
}
v = v.Elem()
t = v.Type()
}

var names []string

switch t.Kind() {
case reflect.Struct:
// Get field names
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.IsExported() {
names = append(names, field.Name)
}
}

// Get method names (use pointer type to get all methods)
ptrType := reflect.PointerTo(t)
for i := 0; i < ptrType.NumMethod(); i++ {
method := ptrType.Method(i)
if method.IsExported() {
names = append(names, method.Name)
}
}

case reflect.Map:
// Get map keys as strings
for _, key := range v.MapKeys() {
keyStr := cast.ToString(key.Interface())
names = append(names, keyStr)
}

default:
// For other types, return empty slice
return []string{}
}

// Sort the names for consistent output
sort.Strings(names)
return names
}

func (ns *Namespace) Timer(name string) Timer {
if ns.timers == nil {
return nopTimer
Expand Down
38 changes: 38 additions & 0 deletions tpl/debug/debug_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,41 @@ Dump: {{ debug.Dump . | safeHTML }}
b := hugolib.TestRunning(t, files)
b.AssertFileContent("public/index.html", "Dump: {\n \"Date\": \"2012-03-15T00:00:00Z\"")
}

func TestDebugList(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.org/"
disableLiveReload = true
-- content/_index.md --
---
title: "The Index"
---
-- layouts/_default/list.html --
{{ $dict := dict "name" "Hugo" "version" "0.120" "type" "SSG" }}
Map Keys: {{ debug.List $dict }}

{{ $page := . }}
Page Fields: {{ debug.List $page }}

Nil: {{ debug.List nil }}
String: {{ debug.List "hello" }}
Number: {{ debug.List 42 }}
Slice: {{ debug.List (slice 1 2 3) }}


`
b := hugolib.TestRunning(t, files)

// Test map keys
b.AssertFileContent("public/index.html", "Map Keys: [name type version]")

// Test that page struct returns field names and methods (should include common page fields)
b.AssertFileContent("public/index.html", "Page Fields:")

// Test edge cases
b.AssertFileContent("public/index.html", "Nil: []")
b.AssertFileContent("public/index.html", "String: []")
b.AssertFileContent("public/index.html", "Number: []")
b.AssertFileContent("public/index.html", "Slice: []")
}
8 changes: 8 additions & 0 deletions tpl/debug/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ func init() {
},
)

ns.AddMethodMapping(ctx.List,
nil,
[][2]string{
{`{{ $s := dict "name" "Hugo" "type" "SSG" }}
{{ debug.List $s }}`, `[name type]`},
},
)

return ns
}

Expand Down