summaryrefslogtreecommitdiffstats
path: root/modules/templates/util_dict.go
blob: c83f22449cdcc40dcca45dd3a2be79c4584b4c11 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package templates

import (
	"fmt"
	"html"
	"html/template"
	"reflect"

	"code.gitea.io/gitea/modules/json"
	"code.gitea.io/gitea/modules/setting"
)

func dictMerge(base map[string]any, arg any) bool {
	if arg == nil {
		return true
	}
	rv := reflect.ValueOf(arg)
	if rv.Kind() == reflect.Map {
		for _, k := range rv.MapKeys() {
			base[k.String()] = rv.MapIndex(k).Interface()
		}
		return true
	}
	return false
}

// dict is a helper function for creating a map[string]any from a list of key-value pairs.
// If the key is dot ".", the value is merged into the base map, just like Golang template's dot syntax: dot means current
// The dot syntax is highly discouraged because it might cause unclear key conflicts. It's always good to use explicit keys.
func dict(args ...any) (map[string]any, error) {
	if len(args)%2 != 0 {
		return nil, fmt.Errorf("invalid dict constructor syntax: must have key-value pairs")
	}
	m := make(map[string]any, len(args)/2)
	for i := 0; i < len(args); i += 2 {
		key, ok := args[i].(string)
		if !ok {
			return nil, fmt.Errorf("invalid dict constructor syntax: unable to merge args[%d]", i)
		}
		if key == "." {
			if ok = dictMerge(m, args[i+1]); !ok {
				return nil, fmt.Errorf("invalid dict constructor syntax: dot arg[%d] must be followed by a dict", i)
			}
		} else {
			m[key] = args[i+1]
		}
	}
	return m, nil
}

func dumpVarMarshalable(v any, dumped map[uintptr]bool) (ret any, ok bool) {
	if v == nil {
		return nil, true
	}
	e := reflect.ValueOf(v)
	for e.Kind() == reflect.Pointer {
		e = e.Elem()
	}
	if e.CanAddr() {
		addr := e.UnsafeAddr()
		if dumped[addr] {
			return "[dumped]", false
		}
		dumped[addr] = true
		defer delete(dumped, addr)
	}
	switch e.Kind() {
	case reflect.Bool, reflect.String,
		reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
		reflect.Float32, reflect.Float64:
		return e.Interface(), true
	case reflect.Struct:
		m := map[string]any{}
		for i := 0; i < e.NumField(); i++ {
			k := e.Type().Field(i).Name
			if !e.Type().Field(i).IsExported() {
				continue
			}
			v := e.Field(i).Interface()
			m[k], _ = dumpVarMarshalable(v, dumped)
		}
		return m, true
	case reflect.Map:
		m := map[string]any{}
		for _, k := range e.MapKeys() {
			m[k.String()], _ = dumpVarMarshalable(e.MapIndex(k).Interface(), dumped)
		}
		return m, true
	case reflect.Array, reflect.Slice:
		var m []any
		for i := 0; i < e.Len(); i++ {
			v, _ := dumpVarMarshalable(e.Index(i).Interface(), dumped)
			m = append(m, v)
		}
		return m, true
	default:
		return "[" + reflect.TypeOf(v).String() + "]", false
	}
}

// dumpVar helps to dump a variable in a template, to help debugging and development.
func dumpVar(v any) template.HTML {
	if setting.IsProd {
		return "<pre>dumpVar: only available in dev mode</pre>"
	}
	m, ok := dumpVarMarshalable(v, map[uintptr]bool{})
	dumpStr := ""
	jsonBytes, err := json.MarshalIndent(m, "", "  ")
	if err != nil {
		dumpStr = fmt.Sprintf("dumpVar: unable to marshal %T: %v", v, err)
	} else if ok {
		dumpStr = fmt.Sprintf("dumpVar: %T\n%s", v, string(jsonBytes))
	} else {
		dumpStr = fmt.Sprintf("dumpVar: unmarshalable %T\n%s", v, string(jsonBytes))
	}
	return template.HTML("<pre>" + html.EscapeString(dumpStr) + "</pre>")
}