forked from gobuffalo/pop
/
associations_for_struct.go
151 lines (126 loc) · 4.12 KB
/
associations_for_struct.go
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
150
151
package associations
import (
"fmt"
"reflect"
"regexp"
"strings"
"github.com/gobuffalo/pop/v6/columns"
)
// If a field match with the regexp, it will be considered as a valid field definition.
// e.g: "MyField" => valid.
// e.g: "MyField.NestedField" => valid.
// e.g: "MyField." => not valid.
// e.g: "MyField.*" => not valid for now.
var validAssociationExpRegexp = regexp.MustCompile(`^(([a-zA-Z0-9]*)(\.[a-zA-Z0-9]+)?)+$`)
// associationBuilders is a map that helps to aisle associations finding process
// with the associations implementation. Every association MUST register its builder
// in this map using its init() method. see ./has_many_association.go as a guide.
var associationBuilders = map[string]associationBuilder{}
// ForStruct returns all associations for
// the struct specified. It takes into account tags
// associations like has_many, belongs_to, has_one.
// it throws an error when it finds a field that does
// not exist for a model.
func ForStruct(s interface{}, fields ...string) (Associations, error) {
t, v := getModelDefinition(s)
if t.Kind() != reflect.Struct {
return nil, fmt.Errorf("could not get struct associations: not a struct but %T", s)
}
fields = trimFields(fields)
associations := Associations{}
fieldsWithInnerAssociation := map[string]InnerAssociations{}
// validate if fields contains a non existing field in struct.
// and verify is it has inner associations.
for i := range fields {
var innerField string
if !validAssociationExpRegexp.MatchString(fields[i]) {
return associations, fmt.Errorf("association '%s' does not match the format %s", fields[i], "'<field>' or '<field>.<nested-field>'")
}
fields[i], innerField = extractFieldAndInnerFields(fields[i])
if _, ok := t.FieldByName(fields[i]); !ok {
return associations, fmt.Errorf("field %s does not exist in model %s", fields[i], t.Name())
}
if innerField != "" {
var found bool
innerF, _ := extractFieldAndInnerFields(innerField)
for j := range fieldsWithInnerAssociation[fields[i]] {
f, _ := extractFieldAndInnerFields(fieldsWithInnerAssociation[fields[i]][j].Fields[0])
if innerF == f {
fieldsWithInnerAssociation[fields[i]][j].Fields = append(fieldsWithInnerAssociation[fields[i]][j].Fields, innerField)
found = true
break
}
}
if !found {
fieldsWithInnerAssociation[fields[i]] = append(fieldsWithInnerAssociation[fields[i]], InnerAssociation{fields[i], []string{innerField}})
}
}
}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
// inline embedded field
if f.Anonymous {
innerAssociations, err := ForStruct(v.Field(i).Interface(), fields...)
if err != nil {
return nil, err
}
associations = append(associations, innerAssociations...)
continue
}
// ignores those fields not included in fields list.
if len(fields) > 0 && fieldIgnoredIn(fields, f.Name) {
continue
}
tags := columns.TagsFor(f)
for name, builder := range associationBuilders {
tag := tags.Find(name)
if !tag.Empty() {
params := associationParams{
field: f,
model: s,
modelType: t,
modelValue: v,
popTags: tags,
innerAssociations: fieldsWithInnerAssociation[f.Name],
}
a, err := builder(params)
if err != nil {
return associations, err
}
associations = append(associations, a)
break
}
}
}
return associations, nil
}
func getModelDefinition(s interface{}) (reflect.Type, reflect.Value) {
v := reflect.ValueOf(s)
v = reflect.Indirect(v)
t := v.Type()
return t, v
}
func trimFields(fields []string) []string {
var trimFields []string
for _, f := range fields {
if strings.TrimSpace(f) != "" {
trimFields = append(trimFields, strings.TrimSpace(f))
}
}
return trimFields
}
func fieldIgnoredIn(fields []string, field string) bool {
for _, f := range fields {
if f == field {
return false
}
}
return true
}
func extractFieldAndInnerFields(field string) (string, string) {
if !strings.Contains(field, ".") {
return field, ""
}
dotIndex := strings.Index(field, ".")
return field[:dotIndex], field[dotIndex+1:]
}