summaryrefslogtreecommitdiff
path: root/form/reflect.go
blob: 4dd401850b23a0d973223ad3c5ff9a8a5a5e345f (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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package form

import (
	"html/template"
	"reflect"
	"strings"
)

// valueOf is basically just reflect.ValueOf, but if the Kind() of the
// value is a pointer or interface it will try to get the reflect.Value
// of the underlying element, and if the pointer is nil it will
// create a new instance of the type and return the reflect.Value of it.
//
// This is used to make the rest of the fields function simpler.
func valueOf(v interface{}) reflect.Value {
	rv := reflect.ValueOf(v)
	// If a nil pointer is passed in but has a type we can recover, but I
	// really should just panic and tell people to fix their shitty code.
	if rv.Type().Kind() == reflect.Pointer && rv.IsNil() {
		rv = reflect.Zero(rv.Type().Elem())
	}
	// If we have a pointer or interface let's try to get the underlying
	// element
	for rv.Kind() == reflect.Pointer || rv.Kind() == reflect.Interface {
		rv = rv.Elem()
	}
	return rv
}

func fields(v interface{}, names ...string) []field {
	rv := valueOf(v)
	if rv.Kind() != reflect.Struct {
		// We can't really do much with a non-struct type. I suppose this
		// could eventually support maps as well, but for now it does not.
		panic("invalid value; only structs are supported")
	}

	t := rv.Type()
	vFields := reflect.VisibleFields(t)
	ret := make([]field, 0, len(vFields))
	for _, tf := range vFields {
		if !tf.IsExported() {
			continue
		}

		rf := rv.FieldByIndex(tf.Index)
		// If this is a nil pointer, create a new instance of the element.
		if tf.Type.Kind() == reflect.Pointer && rf.IsNil() {
			rf = reflect.Zero(tf.Type.Elem())
		}

		// If this is a struct it has nested fields we need to add. The
		// simplest way to do this is to recursively call `fields` but
		// to provide the name of this struct field to be added as a prefix
		// to the fields.
		// This does not apply to anonymous structs, because their fields are
		// seen as "inlined".
		if reflect.Indirect(rf).Kind() == reflect.Struct {
			if !tf.Anonymous {
				ret = append(ret, fields(rf.Interface(), append(names, tf.Name)...)...)
			}
			continue
		}

		// If we are still in this loop then we aren't dealing with a nested
		// struct and need to add the field. First we check to see if the
		// ignore tag is present, then we set default values, then finally
		// we overwrite defaults with any provided tags.
		tags, ignored := parseTags(tf.Tag.Get("form"))
		if ignored {
			continue
		}
		name := append(names, tf.Name)
		f := field{
			Name:        strings.Join(name, "."),
			Label:       tf.Name,
			Placeholder: tf.Name,
			Type:        "text",
			Value:       rf.Interface(),
		}
		f.applyTags(tags)
		ret = append(ret, f)
	}
	return ret
}

func (f *field) applyTags(tags map[string]string) {
	if v, ok := tags["name"]; ok {
		f.Name = v
	}
	if v, ok := tags["label"]; ok {
		f.Label = v
		// DO NOT move this label check after the placeholder check or
		// this will cause issues.
		f.Placeholder = v
	}
	if v, ok := tags["placeholder"]; ok {
		f.Placeholder = v
	}
	if v, ok := tags["type"]; ok {
		f.Type = v
	}
	if v, ok := tags["id"]; ok {
		f.ID = v
	}
	if v, ok := tags["footer"]; ok {
		// Probably shouldn't be HTML but whatever.
		f.Footer = template.HTML(v)
	}
	if v, ok := tags["class"]; ok {
		f.Class = v
	}
	if v, ok := tags["options"]; ok {
		f.Options = strings.Split(v, ",")
	}
}

func parseTags(tags string) (map[string]string, bool) {
	tags = strings.TrimSpace(tags)
	if len(tags) == 0 {
		return map[string]string{}, false
	}
	split := strings.Split(tags, ";")
	ret := make(map[string]string, len(split))
	for _, tag := range split {
		kv := strings.Split(tag, "=")
		if len(kv) < 2 {
			if kv[0] == "-" {
				return nil, true
			}
			continue
		}
		k, v := strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1])
		ret[k] = v
	}
	return ret, false
}

type field struct {
	Name        string
	Label       string
	Placeholder string
	Type        string
	ID          string
	Options     []string
	Value       interface{}
	Footer      template.HTML
	Class       string
}