Repository navigation
Expand file tree
/
Copy pathparams.go
More file actions
127 lines (122 loc) · 3.62 KB
/
Copy pathparams.go
File metadata and controls
127 lines (122 loc) · 3.62 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
package glassnode
import (
"net/url"
"strconv"
"strings"
"time"
)
// MetricParams selects data or filters metadata. Zero fields are omitted.
// Asset is a convenience for one asset; Assets and Exchanges encode repeated
// query parameters (a=BTC&a=ETH). Do not set both Asset and Assets.
// Extra accepts additional documented selectors such as network, quote_symbol,
// bps, side, size, miner and maturity; it must not duplicate a typed field.
type MetricParams struct {
Asset string
Assets []string
Exchanges []string
Since time.Time
Until time.Time
Interval string
Currency string
Extra url.Values
}
// Values returns a fresh query map. Unix seconds may also be supplied through
// Extra["s"] and Extra["u"] when the corresponding time.Time field is zero.
func (p *MetricParams) Values() (url.Values, error) {
if p == nil {
return url.Values{}, nil
}
q, err := cloneQuery(p.Extra)
if err != nil {
return nil, err
}
set := func(key string, values []string) error {
if len(values) == 0 {
return nil
}
if _, exists := q[key]; exists {
return &InputError{key, "set either the typed field or Extra, not both"}
}
q[key] = append([]string(nil), values...)
return nil
}
if p.Asset != "" && len(p.Assets) > 0 {
return nil, &InputError{"asset", "set Asset or Assets, not both"}
}
assets := p.Assets
if p.Asset != "" {
assets = []string{p.Asset}
}
if err := set("a", assets); err != nil {
return nil, err
}
if err := set("e", p.Exchanges); err != nil {
return nil, err
}
for _, pair := range []struct{ key, value string }{{"i", p.Interval}, {"c", p.Currency}} {
if pair.value != "" {
if err := set(pair.key, []string{pair.value}); err != nil {
return nil, err
}
}
}
for _, pair := range []struct {
key string
value time.Time
}{{"s", p.Since}, {"u", p.Until}} {
if !pair.value.IsZero() {
if err := set(pair.key, []string{strconv.FormatInt(pair.value.Unix(), 10)}); err != nil {
return nil, err
}
}
}
for _, key := range []string{"s", "u", "i", "c", "f"} {
if len(q[key]) > 1 {
return nil, &InputError{key, "accepts only one value"}
}
}
var since, until *int64
for _, key := range []string{"s", "u"} {
if raw := q.Get(key); raw != "" {
v, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
return nil, &InputError{key, "expected Unix seconds"}
}
if key == "s" {
since = &v
} else {
until = &v
}
}
}
if since != nil && until != nil && *since > *until {
return nil, &InputError{"time range", "Since must be <= Until"}
}
return cloneQuery(q)
}
func metricQuery(path string, params *MetricParams, metadata bool) (string, url.Values, error) {
return metricQueryFormat(path, params, metadata, "json")
}
// metricQueryFormat is metricQuery with an explicit response format. Only the
// CSV download sets anything other than json; decoded responses must be JSON.
func metricQueryFormat(path string, params *MetricParams, metadata bool, format string) (string, url.Values, error) {
path = "/" + strings.TrimPrefix(path, "/")
if err := validateEndpoint(path); err != nil {
return "", nil, &InputError{"metric path", "expected a relative metric path without URL, query or traversal"}
}
q, err := params.Values()
if err != nil {
return "", nil, err
}
if metadata {
if _, exists := q["path"]; exists {
return "", nil, &InputError{"path", "use the metric path argument"}
}
q.Set("path", path)
}
if existing := q.Get("f"); existing != "" && !strings.EqualFold(existing, format) {
return "", nil, &InputError{"f", "the response format is set by the method; use GetMetricCSV for CSV"}
}
q.Set("f", format)
return path, q, nil
}