-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathparser.go
More file actions
145 lines (123 loc) · 3.48 KB
/
parser.go
File metadata and controls
145 lines (123 loc) · 3.48 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
package gsheets
import (
"fmt"
"iter"
"reflect"
"strings"
"google.golang.org/api/sheets/v4"
)
// Result is a struct that holds the result of a parsing operation.
// It contains the parsed value or an error if any.
type Result[T any] struct {
Val T
Err error
}
// ParseSheetIntoStructs parses a sheet page and returns an iterator over the parsing Result.
// If an error occurs during validation or when fetching data, the function will return it.
// Parsing errors are returned as part of the Result, and can therefore be handled by the caller.
// The iterator will still proceed to the next row, if it isn't stopped.
func ParseSheetIntoStructs[T any](cfg Config, opts ...ConfigOption) (iter.Seq2[int, Result[T]], error) {
results, _, err := parseSheet[T](cfg, opts)
return results, err
}
// ParseSheetIntoStructSlice parses a sheet page and returns a slice of structs with the give type.
// If an error occurs, the function will immediately return it.
func ParseSheetIntoStructSlice[T any](cfg Config, opts ...ConfigOption) ([]T, error) {
results, rows, err := parseSheet[T](cfg, opts)
if err != nil {
return nil, err
}
items := make([]T, 0, rows)
for _, item := range results {
if item.Err != nil {
return nil, item.Err
}
items = append(items, item.Val)
}
return items, cfg.Context().Err()
}
func parseSheet[T any](cfg Config, opts []ConfigOption) (iter.Seq2[int, Result[T]], int, error) {
refT := reflect.TypeFor[T]()
cfg, err := cfg.init(refT, opts)
if err != nil {
return nil, 0, err
}
resp, err := cfg.fetch(cfg)
if err != nil {
return nil, 0, err
}
mappings, err := createMappings(refT, resp.Values[0], cfg)
if err != nil {
return nil, 0, err
}
fillEmptyValues(resp)
ctx := cfg.Context()
return func(yield func(int, Result[T]) bool) {
rows:
for i, row := range resp.Values[1:] {
select {
case <-ctx.Done():
return
default:
rowIdx := i + 2 // 1-based index + first row is captions
var item T
refItem := reflect.ValueOf(&item).Elem()
for _, mapping := range mappings {
val, nonEmpty, err := mapping.convert(row[mapping.colIndex].(string), cfg.datetimeFormats)
if err != nil {
err = &MappingError{
Sheet: cfg.sheetName,
Cell: fmt.Sprintf("%s%d", columnName(mapping.colIndex), rowIdx),
Field: mapping.typeName + "." + mapping.field.Name,
err: err,
}
if !yield(rowIdx, Result[T]{Err: err}) {
return
}
continue rows
}
if !nonEmpty {
continue
}
if mapping.initEmbedPtr != nil {
mapping.initEmbedPtr(refItem)
}
refItem.FieldByIndex(mapping.field.Index).Set(val)
}
if !yield(rowIdx, Result[T]{Val: item}) {
return
}
}
}
}, len(resp.Values[1:]), ctx.Err()
}
func fillEmptyValues(data *sheets.ValueRange) {
var maxWidth int
for _, row := range data.Values {
if len(row) > maxWidth {
maxWidth = len(row)
}
}
for rowIdx, row := range data.Values {
for colIdx := len(row); colIdx < maxWidth; colIdx++ {
data.Values[rowIdx] = append(data.Values[rowIdx], "")
}
}
}
func columnName(index int) string {
index += 1
var res string
for index > 0 {
index--
res = string(rune(index%26+97)) + res
index /= 26
}
return strings.ToUpper(res)
}
type fetchFN func(cfg Config) (*sheets.ValueRange, error)
func fetchViaGoogleAPI(cfg Config) (*sheets.ValueRange, error) {
return cfg.Service.Spreadsheets.Values.Get(cfg.spreadsheetID, cfg.sheetName).
Context(cfg.Context()).
MajorDimension("ROWS").
Do()
}