├── .gitignore ├── LICENSE ├── README.md ├── firebase.go └── firebase_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | keys.go 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Cosmin Nicolaescu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Go Firebase 2 | ======== 3 | 4 | ## Summary 5 | 6 | Helper library for invoking the Firebase REST API. 7 | 8 | ## Installation 9 | 10 | ### Build 11 | 12 | ```sh 13 | go get github.com/cosn/firebase 14 | ``` 15 | 16 | ### Test 17 | 18 | Edit the firebase_test.go file and set the ```testUrl``` and ```testKey``` variables to match your Firebase account. 19 | 20 | Then run: 21 | ```sh 22 | go test github.com/cosn/firebase... 23 | ``` 24 | 25 | ## Usage 26 | 27 | First import the package into your code: 28 | ```go 29 | import ( 30 | "github.com/cosn/firebase" 31 | ) 32 | ``` 33 | 34 | To use the client, initialize it and make requests similarly to the Firebase docs: 35 | ```go 36 | firebase := new(firebase.Client) 37 | firebase.Init("https://.firebase.com", "", nil) 38 | 39 | n := &Name { First: "Jack", Last: "Sparrow" } 40 | jack, err_ := firebase.Child("users/jack", nil, nil).Set("name", n, nil) 41 | ``` 42 | 43 | Currently, the following methods are supported: 44 | ```go 45 | Child(path) 46 | Push(value) 47 | Set(path, value) 48 | Update(path, value) 49 | Remove(path) 50 | Value() 51 | Rules() 52 | SetRules(rules) 53 | ``` 54 | 55 | For more details about this library, see the [GoDoc](http://godoc.org/github.com/cosn/firebase) documentation. 56 | 57 | For more details about the Firebase APIs, see the [Firebase official documentation](https://www.firebase.com/docs/). 58 | -------------------------------------------------------------------------------- /firebase.go: -------------------------------------------------------------------------------- 1 | // Package firebase impleements a RESTful client for Firebase. 2 | package firebase 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "errors" 8 | "io/ioutil" 9 | "log" 10 | "net" 11 | "net/http" 12 | "net/url" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | // Api is the interface for interacting with Firebase. 18 | // Consumers of this package can mock this interface for testing purposes. 19 | type Api interface { 20 | Call(method, path, auth string, body []byte, params map[string]string) ([]byte, error) 21 | } 22 | 23 | // Client is the Firebase client. 24 | type Client struct { 25 | // Url is the client's base URL used for all calls. 26 | Url string 27 | 28 | // Auth is authentication token used when making calls. 29 | // The token is optional and can also be overwritten on an individual 30 | // call basis via params. 31 | Auth string 32 | 33 | // api is the underlying client used to make calls. 34 | api Api 35 | 36 | // value is the value of the object at the current Url 37 | value interface{} 38 | } 39 | 40 | // Rules is the structure for security rules. 41 | type Rules map[string]interface{} 42 | 43 | // f is the internal implementation of the Firebase API client. 44 | type f struct{} 45 | 46 | // suffix is the Firebase suffix for invoking their API via HTTP 47 | const suffix = ".json" 48 | 49 | var ( 50 | connectTimeout = time.Duration(10 * time.Second) // timeout for http connection 51 | readWriteTimeout = time.Duration(10 * time.Second) // timeout for http read/write 52 | ) 53 | 54 | // httpClient is the HTTP client used to make calls to Firebase 55 | var httpClient = newTimeoutClient(connectTimeout, readWriteTimeout) 56 | 57 | // Init initializes the Firebase client with a given root url and optional auth token. 58 | // The initialization can also pass a mock api for testing purposes. 59 | func (c *Client) Init(root, auth string, api Api) { 60 | if api == nil { 61 | api = new(f) 62 | } 63 | 64 | c.api = api 65 | c.Url = root 66 | c.Auth = auth 67 | } 68 | 69 | // Value returns the value of of the current Url. 70 | func (c *Client) Value() interface{} { 71 | // if we have not yet performed a look-up, do it so a value is returned 72 | if c.value == nil { 73 | var v interface{} 74 | c = c.Child("", nil, v) 75 | } 76 | 77 | if c == nil { 78 | return nil 79 | } 80 | 81 | return c.value 82 | } 83 | 84 | // Child returns a populated pointer for a given path. 85 | // If the path cannot be found, a null pointer is returned. 86 | func (c *Client) Child(path string, params map[string]string, v interface{}) *Client { 87 | u := c.Url + "/" + path 88 | 89 | res, err := c.api.Call("GET", u, c.Auth, nil, params) 90 | if err != nil { 91 | return nil 92 | } 93 | 94 | err = json.Unmarshal(res, &v) 95 | if err != nil { 96 | log.Printf("%v\n", err) 97 | return nil 98 | } 99 | 100 | ret := &Client{ 101 | api: c.api, 102 | Auth: c.Auth, 103 | Url: u, 104 | value: v} 105 | 106 | return ret 107 | } 108 | 109 | // Push creates a new value under the current root url. 110 | // A populated pointer with that value is also returned. 111 | func (c *Client) Push(value interface{}, params map[string]string) (*Client, error) { 112 | body, err := json.Marshal(value) 113 | if err != nil { 114 | log.Printf("%v\n", err) 115 | return nil, err 116 | } 117 | 118 | res, err := c.api.Call("POST", c.Url, c.Auth, body, params) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | var r map[string]string 124 | 125 | err = json.Unmarshal(res, &r) 126 | if err != nil { 127 | log.Printf("%v\n", err) 128 | return nil, err 129 | } 130 | 131 | ret := &Client{ 132 | api: c.api, 133 | Auth: c.Auth, 134 | Url: c.Url + "/" + r["name"], 135 | value: value} 136 | 137 | return ret, nil 138 | } 139 | 140 | // Set overwrites the value at the specified path and returns populated pointer 141 | // for the updated path. 142 | func (c *Client) Set(path string, value interface{}, params map[string]string) (*Client, error) { 143 | u := c.Url + "/" + path 144 | 145 | body, err := json.Marshal(value) 146 | if err != nil { 147 | log.Printf("%v\n", err) 148 | return nil, err 149 | } 150 | 151 | res, err := c.api.Call("PUT", u, c.Auth, body, params) 152 | 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | ret := &Client{ 158 | api: c.api, 159 | Auth: c.Auth, 160 | Url: u} 161 | 162 | if len(res) > 0 { 163 | var r interface{} 164 | 165 | err = json.Unmarshal(res, &r) 166 | if err != nil { 167 | log.Printf("%v\n", err) 168 | return nil, err 169 | } 170 | 171 | ret.value = r 172 | } 173 | 174 | return ret, nil 175 | } 176 | 177 | // Update performs a partial update with the given value at the specified path. 178 | func (c *Client) Update(path string, value interface{}, params map[string]string) error { 179 | body, err := json.Marshal(value) 180 | if err != nil { 181 | log.Printf("%v\n", err) 182 | return err 183 | } 184 | 185 | _, err = c.api.Call("PATCH", c.Url+"/"+path, c.Auth, body, params) 186 | 187 | // if we've just updated the root node, clear the value so it gets looked up 188 | // again and populated correctly since we just applied a diffgram 189 | if len(path) == 0 { 190 | c.value = nil 191 | } 192 | 193 | return err 194 | } 195 | 196 | // Remove deletes the data at the given path. 197 | func (c *Client) Remove(path string, params map[string]string) error { 198 | _, err := c.api.Call("DELETE", c.Url+"/"+path, c.Auth, nil, params) 199 | 200 | return err 201 | } 202 | 203 | // Rules returns the security rules for the database. 204 | func (c *Client) Rules(params map[string]string) (Rules, error) { 205 | res, err := c.api.Call("GET", c.Url+"/.settings/rules", c.Auth, nil, params) 206 | if err != nil { 207 | return nil, err 208 | } 209 | 210 | var v Rules 211 | err = json.Unmarshal(res, &v) 212 | if err != nil { 213 | log.Printf("%v\n", err) 214 | return nil, err 215 | } 216 | 217 | return v, nil 218 | } 219 | 220 | // SetRules overwrites the existing security rules with the new rules given. 221 | func (c *Client) SetRules(rules *Rules, params map[string]string) error { 222 | body, err := json.Marshal(rules) 223 | if err != nil { 224 | log.Printf("%v\n", err) 225 | return err 226 | } 227 | 228 | _, err = c.api.Call("PUT", c.Url+"/.settings/rules", c.Auth, body, params) 229 | 230 | return err 231 | } 232 | 233 | // Call invokes the appropriate HTTP method on a given Firebase URL. 234 | func (f *f) Call(method, path, auth string, body []byte, params map[string]string) ([]byte, error) { 235 | if !strings.HasSuffix(path, "/") { 236 | path += "/" 237 | } 238 | 239 | path += suffix 240 | qs := url.Values{} 241 | 242 | // if the client has an auth, set it as a query string. 243 | // the caller can also override this on a per-call basis 244 | // which will happen via params below 245 | if len(auth) > 0 { 246 | qs.Set("auth", auth) 247 | } 248 | 249 | for k, v := range params { 250 | qs.Set(k, v) 251 | } 252 | 253 | if len(qs) > 0 { 254 | path += "?" + qs.Encode() 255 | } 256 | 257 | req, err := http.NewRequest(method, path, bytes.NewReader(body)) 258 | if err != nil { 259 | log.Printf("Cannot create Firebase request: %v\n", err) 260 | return nil, err 261 | } 262 | 263 | req.Close = true 264 | log.Printf("Calling %v %q\n", method, path) 265 | 266 | res, err := httpClient.Do(req) 267 | if err != nil { 268 | log.Printf("Request to Firebase failed: %v\n", err) 269 | return nil, err 270 | } 271 | defer res.Body.Close() 272 | 273 | ret, err := ioutil.ReadAll(res.Body) 274 | if err != nil { 275 | log.Printf("Cannot parse Firebase response: %v\n", err) 276 | return nil, err 277 | } 278 | 279 | if res.StatusCode >= 400 { 280 | err = errors.New(string(ret)) 281 | log.Printf("Error encountered from Firebase: %v\n", err) 282 | return nil, err 283 | } 284 | 285 | return ret, nil 286 | } 287 | 288 | func TimeoutDialer(cTimeout time.Duration, rwTimeout time.Duration) func(net, addr string) (c net.Conn, err error) { 289 | return func(netw, addr string) (net.Conn, error) { 290 | conn, err := net.DialTimeout(netw, addr, cTimeout) 291 | if err != nil { 292 | return nil, err 293 | } 294 | conn.SetDeadline(time.Now().Add(rwTimeout)) 295 | return conn, nil 296 | } 297 | } 298 | 299 | func newTimeoutClient(connectTimeout time.Duration, readWriteTimeout time.Duration) *http.Client { 300 | return &http.Client{ 301 | Transport: &http.Transport{ 302 | Dial: TimeoutDialer(connectTimeout, readWriteTimeout), 303 | }, 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /firebase_test.go: -------------------------------------------------------------------------------- 1 | package firebase 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type Name struct { 8 | First string `json:",omitempty"` 9 | Last string `json:",omitempty"` 10 | } 11 | 12 | /* 13 | Set the two variables below and set them to your own 14 | Firebase URL and credentials (optional) if you're forking the code 15 | and want to test your changes. 16 | */ 17 | 18 | // enter your firebase credentials for testing. 19 | var ( 20 | testUrl string = "" 21 | testAuth string = "" 22 | ) 23 | 24 | func TestValue(t *testing.T) { 25 | client := new(Client) 26 | client.Init(testUrl+"/tests", testAuth, nil) 27 | 28 | r := client.Value() 29 | 30 | if r == nil { 31 | t.Fatalf("No values returned from the server\n") 32 | } 33 | } 34 | 35 | func TestChild(t *testing.T) { 36 | client := new(Client) 37 | client.Init(testUrl+"/tests", testAuth, nil) 38 | 39 | r := client.Child("", nil, nil) 40 | 41 | if r == nil { 42 | t.Fatalf("No child returned from the server\n") 43 | } 44 | } 45 | 46 | func TestPush(t *testing.T) { 47 | client := new(Client) 48 | client.Init(testUrl+"/tests", testAuth, nil) 49 | 50 | name := &Name{First: "FirstName", Last: "LastName"} 51 | 52 | r, err := client.Push(name, nil) 53 | 54 | if err != nil { 55 | t.Fatalf("%v\n", err) 56 | } 57 | 58 | if r == nil { 59 | t.Fatalf("No client returned from the server\n") 60 | } 61 | } 62 | 63 | func TestSet(t *testing.T) { 64 | c1 := new(Client) 65 | c1.Init(testUrl+"/tests/users", testAuth, nil) 66 | 67 | name := &Name{First: "First", Last: "last"} 68 | c2, _ := c1.Push(name, nil) 69 | 70 | newName := &Name{First: "NewFirst", Last: "NewLast"} 71 | r, err := c2.Set("", newName, map[string]string{"print": "silent"}) 72 | 73 | if err != nil { 74 | t.Fatalf("%v\n", err) 75 | } 76 | 77 | if r == nil { 78 | t.Fatalf("No client returned from the server\n") 79 | } 80 | } 81 | 82 | func TestUpdate(t *testing.T) { 83 | c1 := new(Client) 84 | c1.Init(testUrl+"/tests/users", testAuth, nil) 85 | 86 | name := &Name{First: "First", Last: "last"} 87 | c2, _ := c1.Push(name, nil) 88 | 89 | newName := &Name{Last: "NewLast"} 90 | err := c2.Update("", newName, nil) 91 | 92 | if err != nil { 93 | t.Fatalf("%v\n", err) 94 | } 95 | } 96 | 97 | func TestRemovet(t *testing.T) { 98 | c1 := new(Client) 99 | c1.Init(testUrl+"/tests/users", testAuth, nil) 100 | 101 | name := &Name{First: "First", Last: "last"} 102 | c2, _ := c1.Push(name, nil) 103 | 104 | err := c2.Remove("", nil) 105 | 106 | if err != nil { 107 | t.Fatalf("%v\n", err) 108 | } 109 | } 110 | 111 | func TestRules(t *testing.T) { 112 | client := new(Client) 113 | client.Init(testUrl, testAuth, nil) 114 | 115 | r, err := client.Rules(nil) 116 | 117 | if err != nil { 118 | t.Fatalf("Error retrieving rules: %v\n", err) 119 | } 120 | 121 | if r == nil { 122 | t.Fatalf("No child returned from the server\n") 123 | } 124 | } 125 | 126 | func TestSetRules(t *testing.T) { 127 | client := new(Client) 128 | client.Init(testUrl, testAuth, nil) 129 | 130 | rules := &Rules{ 131 | "rules": map[string]string{ 132 | ".read": "auth.username == 'admin'", 133 | ".write": "auth.username == 'admin'", 134 | }, 135 | } 136 | 137 | err := client.SetRules(rules, nil) 138 | 139 | if err != nil { 140 | t.Fatalf("Error retrieving rules: %v\n", err) 141 | } 142 | } 143 | --------------------------------------------------------------------------------