forked from zabawaba99/firego
/
firebase.go
178 lines (154 loc) · 4.17 KB
/
firebase.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
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
/*
Package firego is a REST client for Firebase (https://firebase.com).
*/
package firego
import (
"bytes"
"errors"
"io/ioutil"
"net"
"net/http"
_url "net/url"
"strings"
"sync"
"time"
)
// TimeoutDuration is the length of time any request will have to establish
// a connection and receive headers from Firebase before returning
// an ErrTimeout error
var TimeoutDuration = 30 * time.Second
// ErrTimeout is an error type is that is returned if a request
// exceeds the TimeoutDuration configured
type ErrTimeout struct {
error
}
// query parameter constants
const (
authParam = "auth"
formatParam = "format"
shallowParam = "shallow"
formatVal = "export"
)
// Firebase represents a location in the cloud
type Firebase struct {
url string
params _url.Values
client *http.Client
watchMtx sync.Mutex
watching bool
stopWatching chan struct{}
}
func sanitizeURL(url string) string {
if !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") {
url = "https://" + url
}
if strings.HasSuffix(url, "/") {
url = url[:len(url)-1]
}
return url
}
// New creates a new Firebase reference. Using a custom client invalidates the
// TimeoutDuration guarantee.
func New(url string, client *http.Client) *Firebase {
if client == nil {
var tr *http.Transport
tr = &http.Transport{
Dial: func(network, address string) (net.Conn, error) {
start := time.Now()
c, err := net.DialTimeout(network, address, TimeoutDuration)
tr.ResponseHeaderTimeout = TimeoutDuration - time.Since(start)
return c, err
},
}
client = &http.Client{Transport: tr}
}
return &Firebase{
url: sanitizeURL(url),
params: _url.Values{},
client: client,
stopWatching: make(chan struct{}),
}
}
// String returns the string representation of the
// Firebase reference
func (fb *Firebase) String() string {
return fb.url
}
// Child creates a new Firebase reference for the requested
// child with the same configuration as the parent
func (fb *Firebase) Child(child string) *Firebase {
return &Firebase{
url: fb.url + "/" + child,
params: fb.params,
client: fb.client,
stopWatching: make(chan struct{}),
}
}
// Shallow limits the depth of the data returned when calling Value.
// If the data at the location is a JSON primitive (string, number or boolean),
// its value will be returned. If the data is a JSON object, the values
// for each key will be truncated to true.
//
// Reference https://www.firebase.com/docs/rest/api/#section-param-shallow
func (fb *Firebase) Shallow(v bool) {
if v {
fb.params.Set(shallowParam, "true")
} else {
fb.params.Del(shallowParam)
}
}
// IncludePriority determines whether or not to ask Firebase
// for the values priority. By default, the priority is not returned
//
// Reference https://www.firebase.com/docs/rest/api/#section-param-format
func (fb *Firebase) IncludePriority(v bool) {
if v {
fb.params.Set(formatParam, formatVal)
} else {
fb.params.Del(formatParam)
}
}
func (fb *Firebase) makeRequest(method string, body []byte) (*http.Request, error) {
path := fb.url + "/.json"
if len(fb.params) > 0 {
path += "?" + fb.params.Encode()
}
return http.NewRequest(method, path, bytes.NewReader(body))
}
func (fb *Firebase) doRequest(method string, body []byte) ([]byte, error) {
req, err := fb.makeRequest(method, body)
if err != nil {
return nil, err
}
resp, err := fb.client.Do(req)
switch err := err.(type) {
default:
return nil, err
case nil:
// carry on
case *_url.Error:
// `http.Client.Do` will return a `url.Error` that wraps a `net.Error`
// when exceeding it's `Transport`'s `ResponseHeadersTimeout`
e1, ok := err.Err.(net.Error)
if ok && e1.Timeout() {
return nil, ErrTimeout{err}
}
return nil, err
case net.Error:
// `http.Client.Do` will return a `net.Error` directly when Dial times
// out, or when the Client's RoundTripper otherwise returns an err
if err.Timeout() {
return nil, ErrTimeout{err}
}
return nil, err
}
defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode/200 != 1 {
return nil, errors.New(string(respBody))
}
return respBody, nil
}