-
Notifications
You must be signed in to change notification settings - Fork 1
/
generator.go
183 lines (143 loc) · 5.06 KB
/
generator.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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package gohateoas
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"regexp"
"strings"
"github.com/survivorbat/go-tsyncmap"
)
// typeCacheMap is used to easily fetch json keys from a type
var typeCacheMap = &tsyncmap.Map[string, map[string]string]{}
// iKind is an abstraction of reflect.Value and reflect.Type that allows us to make ensureConcrete generic.
type iKind[T any] interface {
Kind() reflect.Kind
Elem() T
}
// ensureConcrete ensures that the given value is a value and not a pointer, if it is, convert it to its element type
//
//nolint:ireturn // Does not matter in this context
func ensureConcrete[T iKind[T]](value T) T {
if value.Kind() == reflect.Ptr {
return ensureConcrete[T](value.Elem())
}
return value
}
// errNotAStruct can be exported in the future if need be
var errNotAStruct = errors.New("object is not a struct")
// getFieldNameFromJson returns the field name from the json tag
func getFieldNameFromJson(object any, jsonKey string) (string, error) {
typeInfo := ensureConcrete(reflect.TypeOf(object))
if typeInfo.Kind() != reflect.Struct {
return "", errNotAStruct
}
typeName := typeNameOf(object)
// Check for cached values, this way we don't need to perform reflection
// every time we want to get the field name from a json key.
if cachedValue, ok := typeCacheMap.Load(typeName); ok {
return cachedValue[jsonKey], nil
}
// It does not
typeCache := map[string]string{}
for i := 0; i < typeInfo.NumField(); i++ {
// Get the json tags of this field
jsonKey := typeInfo.Field(i).Tag.Get("json")
// Ignore empty json fields
if jsonKey == "" || jsonKey == "-" {
continue
}
// Take the first item from the list and use that as the json key, save it
// in the cache map.
jsonProperty := strings.Split(jsonKey, ",")[0]
typeCache[jsonProperty] = typeInfo.Field(i).Name
}
typeCacheMap.Store(typeName, typeCache)
return typeCache[jsonKey], nil
}
// tokenReplaceRegex is a regex that matches tokens in the form of {token}
var tokenReplaceRegex = regexp.MustCompile(`{([^{}]*)}`)
// injectLinks injects the actual links into the struct if any are registered
func injectLinks(registry LinkRegistry, object any, result map[string]any) {
// Add links if there are any
links := registry[typeNameOf(object)]
if len(links) == 0 {
return
}
linkMap := map[string]LinkInfo{}
// Loop through every link and inject it into the object, replacing tokens
// with the appropriate values.
for linkType, linkInfo := range links {
// Find matches for tokens in the linkInfo like {id} or {name}
matches := tokenReplaceRegex.FindAllStringSubmatch(linkInfo.Href, -1)
for _, match := range matches {
// Check if the value is in the object, like "id" or "name"
urlValue, ok := result[match[1]]
if !ok {
continue
}
// Replace the {name} with the actual value
matchString := fmt.Sprintf("{%s}", match[1])
linkInfo.Href = strings.ReplaceAll(linkInfo.Href, matchString, fmt.Sprintf("%v", urlValue))
}
// Save the linkInfo in the object
linkMap[linkType] = linkInfo
}
// Add the _links property
result["_links"] = linkMap
}
// walkThroughObject goes through the object and injects links into the structs it comes across
func walkThroughObject(registry LinkRegistry, object any, result any) {
// Prevent nil pointer dereference
if result == nil {
return
}
// We use this to dissect the object
reflectValue := ensureConcrete(reflect.ValueOf(object))
switch result := result.(type) {
case []any:
// Loop through the slice's entries and recursively walk through those objects
for index := range result {
walkThroughObject(registry, ensureConcrete(reflectValue.Index(index)).Interface(), result[index])
}
case map[string]any:
// Actually inject links, since this is a struct
injectLinks(registry, object, result)
// Loop through the map's entries and recursively walk through those objects
for jsonKey, value := range result {
switch resultCastValue := value.(type) {
case map[string]any, []any:
fieldName, err := getFieldNameFromJson(object, jsonKey)
if err != nil {
continue
}
fieldValue := reflectValue.FieldByName(fieldName)
if !fieldValue.IsValid() {
continue
}
walkThroughObject(registry, fieldValue.Interface(), resultCastValue)
}
}
}
}
// InjectLinks is similar to json.Marshal, but it will inject links into the response if the
// registry has any links for the given type. It does this recursively.
func InjectLinks(registry LinkRegistry, object any) []byte {
rawResponseJson, _ := json.Marshal(object)
// If the registry is empty, don't bother doing any reflection
if len(registry) == 0 {
return rawResponseJson
}
var resultObject any
//nolint:exhaustive // Doesn't make sense to add more here
switch ensureConcrete(reflect.ValueOf(object)).Kind() {
case reflect.Slice, reflect.Struct, reflect.Array:
_ = json.Unmarshal(rawResponseJson, &resultObject)
walkThroughObject(registry, object, resultObject)
default:
// Prevent unnecessary json.Marshal
return rawResponseJson
}
finalResponse, _ := json.Marshal(resultObject)
return finalResponse
}