-
Notifications
You must be signed in to change notification settings - Fork 0
/
resultset.go
128 lines (113 loc) · 4.08 KB
/
resultset.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
package aggro
import (
"fmt"
"strings"
)
// Resultset represents a complete set of result buckets and any associated errors.
type Resultset struct {
Errors []error `json:"errors"`
Buckets []*ResultBucket `json:"buckets"`
Composition []interface{} `json:"-"`
}
// ResultBucket represents recursively built metrics for our tablular data.
type ResultBucket struct {
Value string `json:"value"`
Metrics map[string]interface{} `json:"metrics"`
Buckets []*ResultBucket `json:"buckets"`
bucketLookup map[string]*ResultBucket
sourceRows []map[string]Cell
}
// ResultTable represents a Resultset split into row / columns at a depth.
type ResultTable struct {
Rows [][]map[string]interface{} `json:"rows"`
RowTitles [][]string `json:"row_titles"`
ColumnTitles [][]string `json:"column_titles"`
}
// Concrete errors.
var (
ErrTargetDepthTooLow = fmt.Errorf("Tabulate: target depth should be 1 or above")
ErrTargetDepthNotReached = fmt.Errorf("Tabulate: reached deepest bucket before hitting target depth")
)
// Tabulate takes a Resultset and converts it to tabular data.
func Tabulate(results *Resultset, depth int) (*ResultTable, error) {
if depth < 1 {
return nil, ErrTargetDepthTooLow
}
// Create our table.
table := &ResultTable{
Rows: [][]map[string]interface{}{},
RowTitles: [][]string{},
ColumnTitles: [][]string{},
}
// And a lookup helper instance.
lookup := &resultLookup{
cells: map[string]map[string]interface{}{},
rowLookup: map[string]bool{},
columnLookup: map[string]bool{},
}
// Recursively build the lookup for each of the root result buckets.
for _, bucket := range results.Buckets {
err := buildLookup([]string{}, 1, depth, table, lookup, bucket)
if err != nil {
return nil, err
}
}
// Now build up the cells for each of the row / column tuples.
for _, row := range table.RowTitles {
tableRow := []map[string]interface{}{}
for _, column := range table.ColumnTitles {
tableRow = append(tableRow, lookup.cells[strings.Join(row, lookupKeyDelimiter)+lookupKeyDelimiter+strings.Join(column, lookupKeyDelimiter)])
}
table.Rows = append(table.Rows, tableRow)
}
// And we're done 👌.
return table, nil
}
// resultLookup stores specific data as the result set is recursively iterated over.
type resultLookup struct {
cells map[string]map[string]interface{}
rowLookup map[string]bool
columnLookup map[string]bool
}
// lookupKeyDelimiter is used to flatten a string array to a single key.
const lookupKeyDelimiter = "😡"
// buildLookup is a recursive function that breaks data into rows and columns
// at a specific depth.
func buildLookup(key []string, depth, targetDepth int, table *ResultTable, lookup *resultLookup, bucket *ResultBucket) error {
// Add the new bucket value to the lookup key.
key = append(key, bucket.Value)
// If we have no buckets, we're at a metric point.
if len(bucket.Buckets) == 0 {
if depth <= targetDepth {
return ErrTargetDepthNotReached
}
// The column key is made up of just the key parts from the target depth.
columnKey := strings.Join(key[targetDepth:], lookupKeyDelimiter)
// If we haven't seen this column tuple before, add it to the lookup.
if _, ok := lookup.columnLookup[columnKey]; !ok {
table.ColumnTitles = append(table.ColumnTitles, key[targetDepth:])
lookup.columnLookup[columnKey] = true
}
m := bucket.Metrics
lookup.cells[strings.Join(key, lookupKeyDelimiter)] = m
return nil
}
// If we've reached target depth, add this key to the rows if it's not there.
if depth == targetDepth {
rowKey := strings.Join(key, lookupKeyDelimiter)
if _, ok := lookup.rowLookup[rowKey]; !ok {
table.RowTitles = append(table.RowTitles, key)
lookup.rowLookup[rowKey] = true
}
}
// Now continue down the 🐇 hole with the next depth of result buckets.
for _, bucket := range bucket.Buckets {
newKey := make([]string, len(key))
copy(newKey, key)
err := buildLookup(newKey, depth+1, targetDepth, table, lookup, bucket)
if err != nil {
return err
}
}
return nil
}