-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathxlsx_row_parser.go
More file actions
326 lines (298 loc) · 10.5 KB
/
Copy pathxlsx_row_parser.go
File metadata and controls
326 lines (298 loc) · 10.5 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
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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
// Package xlsxcfg converts Excel (.xlsx) sheets into Protocol Buffer-defined
// config data. Sheets are read row-wise (or column-wise for transposed sheets),
// then mapped to nested structures via closure-based cell-to-field dispatch.
package xlsxcfg
import (
"context"
"fmt"
"strconv"
"google.golang.org/protobuf/reflect/protoreflect"
)
// rowParser transforms Excel data rows into nested *OrderedMap structures mirroring
// proto message layouts. During Meta(), it processes the header row to build
// closure-based cell-to-field mappings that avoid reflection. During Parse(), it
// invokes those closures with actual cell values.
type rowParser struct {
param *Config
// isStr reports whether the field at the given dot-separated typePath is a
// string type. Captures the proto type name so callers don't need to pass it.
isStr func(fieldPath string) bool
getFieldDesc func(fieldPath string) protoreflect.FieldDescriptor
// errStack accumulates parse errors during a Parse() call. Only the first error is returned.
errStack []error
// hasMeta indicates whether Meta() has been called.
hasMeta bool
res *OrderedMap
// set is an ordered slice of setter closures, one per column. Built during Meta().
set []func(v string)
// fnGet maps a full ident path to a getter closure that lazily creates intermediate
// maps/slices as needed.
fnGet map[string]func() any
fnSet map[string]func(v any)
}
// newRowParser creates a rowParser for the given proto message type name.
func newRowParser(typeName string, param *Config) *rowParser {
return &rowParser{
param: param,
isStr: func(fieldPath string) bool {
return param.IsStrField(typeName, fieldPath)
},
getFieldDesc: func(fieldPath string) protoreflect.FieldDescriptor {
return param.GetFieldDescriptor(typeName, fieldPath)
},
}
}
// Meta processes the header row to build column-to-field mappings. Header cells
// use dot-separated paths (e.g., "Phone.Region") for nested structs and "#N"
// tokens for 1-based list indices. Must be called before Parse().
func (p *rowParser) Meta(ctx context.Context, row []string) {
p.fnGet = make(map[string]func() any, len(row))
p.fnSet = make(map[string]func(v any), len(row))
p.set = make([]func(v string), 0, len(row))
p.res = NewOrderedMap(len(row))
// Root getter: empty string key returns the top-level result map.
p.fnGet[""] = func() any { return p.res }
for _, meta := range row {
if meta == "" {
p.set = append(p.set, func(_ string) {})
continue
}
tr := newTokenReader(meta)
for tr.Next() {
ident := tr.Ident()
prevIdent := tr.FullPrev()
fullIdent := tr.FullIdent()
typePath := tr.TypePath()
// Dispatch: list index → metaList, intermediate struct → metaStruct, leaf → metaField.
if ai := tr.ListIndex(); ai >= 0 {
p.metaList(ident, prevIdent, fullIdent, typePath, ai-1, !tr.HasNext())
} else if tr.HasNext() {
p.metaStruct(ident, prevIdent, fullIdent, typePath)
} else {
p.metaField(ident, prevIdent, fullIdent, typePath)
}
}
}
p.hasMeta = true
}
// Parse processes one data row using the closures built by Meta().
// Returns nil for rows where all cells are empty.
func (p *rowParser) Parse(ctx context.Context, row []string) (*OrderedMap, error) {
if !p.hasMeta {
return nil, fmt.Errorf("row parser no metadata")
}
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
p.res = NewOrderedMap(len(p.set))
p.errStack = nil
// Trim trailing empty cells.
ni := len(row) - 1
for ; ni >= 0; ni-- {
if row[ni] != "" {
break
}
}
if ni < 0 {
return nil, nil
}
row = row[:ni+1]
for i, cell := range row {
if i < len(p.set) {
p.set[i](cell)
}
}
if len(p.errStack) > 0 {
return nil, p.errStack[0]
}
return p.res, nil
}
// resolveEnumValue resolves a cell value for a proto enum field.
// Tries integer parse first (backward compatible), then enum value name lookup.
func resolveEnumValue(fd protoreflect.FieldDescriptor, cellValue string) (int64, error) {
if n, err := strconv.ParseInt(cellValue, 10, 64); err == nil {
return n, nil
}
ed := fd.Enum()
if ed == nil {
return 0, fmt.Errorf("field %q is not an enum", fd.Name())
}
evd := ed.Values().ByName(protoreflect.Name(cellValue))
if evd == nil {
return 0, fmt.Errorf("enum value %q not found in %s", cellValue, ed.Name())
}
return int64(evd.Number()), nil
}
// convertVal converts a cell's string value to the appropriate Go type based on
// the proto field type. Empty values in non-string fields default to "0". Constant
// references (e.g., [Key]) are resolved before type conversion.
//
// When value_convert is enabled and the target field is a scalar numeric or boolean
// proto kind, the post-constant cell text is looked up in the configured maps
// (to_number / to_bool) and the mapped value is returned immediately — bypassing
// strconv.ParseInt and the parse-error path. Map lookup is case-sensitive and a
// miss falls through to the existing parse behavior unchanged.
func (p *rowParser) convertVal(typePath, fullIdent, v string) any {
if p.param.ConstData != nil {
if resolved, ok := p.param.ConstData.Get(v); ok {
v = resolved
}
}
vc := p.param.ConfigFile.ValueConvert
var fd protoreflect.FieldDescriptor
if vc.Enabled {
fd = p.getFieldDesc(typePath)
}
var res any
if p.isStr(typePath) {
res = v
} else if fd != nil && fd.Kind() == protoreflect.EnumKind {
if v == "" {
v = "0"
}
n, e := resolveEnumValue(fd, v)
if e != nil {
p.errStack = append(p.errStack, fmt.Errorf("parse col[%s]: %v", fullIdent, e))
} else {
res = n
}
} else if fd != nil && vc.Enabled && isValueConvertKind(fd.Kind()) {
// Hit returns the mapped typed value (int64 for numeric kinds, bool for BoolKind)
// without touching errStack. Miss (zero value + !ok) falls through to the
// numeric parse path below, preserving the existing "parse col[...] as number
// failed" error for non-numeric cells the user did not explicitly cover.
if mapped, ok := lookupValueConvert(vc, fd.Kind(), v); ok {
return mapped
}
if v == "" {
v = "0"
}
n, e := strconv.ParseInt(v, 10, 64)
if e != nil {
p.errStack = append(p.errStack, fmt.Errorf("parse col[%s] as number failed: %v", fullIdent, e))
} else {
res = n
}
} else {
if v == "" {
v = "0"
}
n, e := strconv.ParseInt(v, 10, 64)
if e != nil {
p.errStack = append(p.errStack, fmt.Errorf("parse col[%s] as number failed: %v", fullIdent, e))
} else {
res = n
}
}
return res
}
// isValueConvertKind reports whether the proto kind participates in value_convert
// mapping. Only scalar numeric and boolean kinds are in scope; string, enum,
// message, and container kinds deliberately fall through to the existing parse.
func isValueConvertKind(k protoreflect.Kind) bool {
switch k {
case protoreflect.BoolKind,
protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind,
protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind,
protoreflect.Uint32Kind, protoreflect.Fixed32Kind,
protoreflect.Uint64Kind, protoreflect.Fixed64Kind,
protoreflect.FloatKind, protoreflect.DoubleKind:
return true
}
return false
}
// lookupValueConvert consults the configured maps for the given proto kind.
// BoolKind reads ToBool; any numeric kind reads ToNumber. Returns (zero, false)
// on a miss so callers can fall through to the existing parse path unchanged.
func lookupValueConvert(vc ValueConvert, k protoreflect.Kind, cell string) (any, bool) {
if k == protoreflect.BoolKind {
v, ok := vc.ToBool[cell]
return v, ok
}
n, ok := vc.ToNumber[cell]
return n, ok
}
// metaList registers getter/setter closures for a list (repeated field) segment.
// Handles slice navigation and auto-expansion: the slice is grown to fit the
// requested index, preserving existing elements.
func (p *rowParser) metaList(ident, prevIdent, fullIdent, typePath string, idx int, isEnd bool) {
p.fnGet[fullIdent] = func() any {
return growListFor(p.ensureParent(prevIdent), ident, idx)[idx]
}
p.fnSet[fullIdent] = func(v any) {
growListFor(p.ensureParent(prevIdent), ident, idx)[idx] = v
}
if !isEnd {
return
}
p.set = append(p.set, func(v string) {
if v == "" {
return
}
p.fnSet[fullIdent](p.convertVal(typePath, fullIdent, v))
})
}
// metaStruct registers getter/setter closures for an intermediate struct node
// (e.g., "Phone" in "Phone.Region"), enabling downstream handlers to navigate into it.
// Both closures lazily create the parent OrderedMap (via ensureParent) so that
// arbitrarily deep nesting (e.g., "A.B.C") and repeated-message nesting
// (e.g., "Items#1.Sub.Name") compose without nil-interface panics; previously the
// getter asserted .(*OrderedMap) on a possibly-nil parent and crashed.
func (p *rowParser) metaStruct(ident, prevIdent, fullIdent, typePath string) {
p.fnGet[fullIdent] = func() any {
m := p.ensureParent(prevIdent)
if v, ok := m.Get(ident); ok && v != nil {
return v
}
child := NewOrderedMap(4)
m.Set(ident, child)
return child
}
p.fnSet[fullIdent] = func(v any) {
p.ensureParent(prevIdent).Set(ident, v)
}
}
// metaField handles a terminal leaf field, appending a setter closure that lazily
// creates the parent struct, converts the cell value, and writes it.
func (p *rowParser) metaField(ident, prevIdent, fullIdent, typePath string) {
p.set = append(p.set, func(v string) {
if v == "" {
return
}
p.ensureParent(prevIdent).Set(ident, p.convertVal(typePath, fullIdent, v))
})
}
// ensureParent returns the *OrderedMap at prevIdent, lazily creating and storing
// an empty OrderedMap (via the registered setter) when the path currently resolves
// to nil. This is what lets getter/setter closures descend into struct parents
// that have not been populated yet — required for ≥3-level nesting and for
// repeated-message fields with nested sub-fields. Safe to call with the root
// path (""), whose getter always returns p.res.
func (p *rowParser) ensureParent(prevIdent string) *OrderedMap {
prev := p.fnGet[prevIdent]()
if prev == nil {
prev = NewOrderedMap(4)
p.fnSet[prevIdent](prev)
}
return prev.(*OrderedMap)
}
// growListFor returns the []any stored at ident in parent, growing it in place
// (preserving existing elements) when it is missing, mistyped, or shorter than
// idx+1, and writing it back to parent. The returned slice always has len > idx.
func growListFor(parent *OrderedMap, ident string, idx int) []any {
if l, ok := parent.Get(ident); ok {
if s, ok := l.([]any); ok && len(s) > idx {
return s
}
}
newList := make([]any, idx+1)
if l, _ := parent.Get(ident); l != nil {
if s, ok := l.([]any); ok {
copy(newList, s)
}
}
parent.Set(ident, newList)
return newList
}