-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathconv.go
More file actions
126 lines (111 loc) · 3.46 KB
/
Copy pathconv.go
File metadata and controls
126 lines (111 loc) · 3.46 KB
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
package conv
import (
"reflect"
"strings"
"unicode/utf8"
)
const commitMessageMaxRunes = 100
// TruncateValidUTF8 sanitizes s so it can be stored in the task.commit_message
// column (VARCHAR(100)): NUL bytes are removed, invalid UTF-8 byte sequences are
// replaced with U+FFFD, and the result is capped at commitMessageMaxRunes runes.
//
// This is not a general-purpose string helper: the hard cap is tied to the
// commit-message column width, so reusing it elsewhere would truncate at 100
// runes unexpectedly.
//
// It guards two failure modes that both make PostgreSQL reject the value with
// `invalid byte sequence for encoding "UTF8"` and abort the transaction:
// - Byte slicing can split a multi-byte character; slicing by runes cannot.
// - Text originating from a non-UTF-8 encoding (e.g. latin-1) can already
// contain invalid bytes even when short, so it is sanitized regardless of
// length.
func TruncateValidUTF8(s string) string {
// Decode at most the first commitMessageMaxRunes runes we intend to keep.
// Converting the whole (possibly very large) message to []rune just to
// discard all but the leading runes wastes CPU and allocations, since
// callers no longer truncate before reaching here.
var b strings.Builder
// Cap the initial allocation: the kept runes never exceed
// commitMessageMaxRunes*utf8.UTFMax bytes, and shorter inputs need even less.
grow := commitMessageMaxRunes * utf8.UTFMax
if len(s) < grow {
grow = len(s)
}
b.Grow(grow)
count := 0
// Ranging over a string decodes runes and yields U+FFFD for invalid UTF-8
// bytes (matching a []rune conversion), so the result is always storable as
// UTF-8 without splitting a multi-byte character.
for _, r := range s {
// PostgreSQL text/varchar cannot store NUL, even though it is valid UTF-8.
if r == 0 {
continue
}
b.WriteRune(r)
count++
if count == commitMessageMaxRunes {
break
}
}
return b.String()
}
func ConvertFloatToIntIfPossible(v any) (int64, bool) {
switch v := v.(type) {
case float64:
f := v
i := int64(f)
if float64(i) == f {
return i, true
}
case float32:
f := v
i := int64(f)
if float32(i) == f {
return i, true
}
}
return 0, false
}
func StructToFlatMap(obj any) map[string]any {
result := make(map[string]any)
val := reflect.ValueOf(obj)
typ := reflect.TypeOf(obj)
if typ.Kind() == reflect.Ptr {
val = val.Elem()
typ = typ.Elem()
}
if typ.Kind() != reflect.Struct {
return result
}
// Iterate over the struct fields
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)
jsonTag := fieldType.Tag.Get("json")
// Use the json tag if it is set, otherwise use the field name
fieldName := jsonTag
if fieldName == "" || fieldName == "-" {
fieldName = fieldType.Name
} else {
// Handle the case where the json tag might have options like `json:"name,omitempty"`
fieldName = strings.Split(fieldName, ",")[0]
}
// Check if the field is a struct itself
if field.Kind() == reflect.Struct {
// Convert nested struct to map
nestedMap := StructToFlatMap(field.Interface())
// Add nested map to result with a prefixed key
for k, v := range nestedMap {
result[fieldName+"."+k] = v
}
} else if (field.Kind() == reflect.Ptr ||
field.Kind() == reflect.Array ||
field.Kind() == reflect.Slice ||
field.Kind() == reflect.Map) && field.IsNil() {
result[fieldName] = nil
} else {
result[fieldName] = field.Interface()
}
}
return result
}