├── .gitignore ├── .gitmodules ├── .travis.yml ├── CHANGES.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── TODO.md ├── doc └── intro.md ├── example └── daemon.rb ├── lib ├── rest-firebase.rb └── rest-firebase │ ├── client.rb │ └── error.rb ├── rest-firebase.gemspec └── test └── test_api.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /pkg/ 2 | /coverage/ 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "rest-core"] 2 | path = rest-core 3 | url = git://github.com/godfat/rest-core.git 4 | [submodule "task"] 5 | path = task 6 | url = git://github.com/godfat/gemgem.git 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | 4 | install: 'gem install bundler; bundle install --retry=3' 5 | script: 'ruby -vr bundler/setup -S rake test' 6 | 7 | matrix: 8 | include: 9 | - rvm: 2.4 10 | - rvm: 2.5 11 | - rvm: 2.6 12 | - rvm: ruby-head 13 | - rvm: jruby 14 | env: JRUBY_OPTS=--debug 15 | 16 | allow_failures: 17 | - rvm: 2.4 18 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # CHANGES 2 | 3 | ## rest-firebase 1.1.0 -- 2016-02-04 4 | 5 | * Adopted rest-core 4.0.0 6 | 7 | ## rest-firebase 1.0.3 -- 2015-11-16 8 | 9 | ### Bugs fixed 10 | 11 | * Raise the default `max_redirects` from 1 to 5 because Firebase introduced 12 | an extra redirect for EventSource. 2 is enough in theory but to make it 13 | more future compatible, we set the default to 5 for now. If Firebase 14 | really needs more than 5 redirects, you could also workaround this by 15 | setting `max_redirects` to another number while setting up the client. 16 | For example: 17 | 18 | ``` ruby 19 | client = RestFirebase.new(:max_redirects => 10) 20 | # or 21 | client = RestFirebase.new 22 | client.max_redirects = 10 23 | ``` 24 | 25 | This works for any version of rest-firebase. Thanks @chanibarin 26 | See: 27 | 28 | ## rest-firebase 1.0.2 -- 2015-06-12 29 | 30 | * Fixed a bug where it would try to encode JSON twice upon retrying. 31 | 32 | ## rest-firebase 1.0.1 -- 2015-01-04 33 | 34 | * Ruby 2.2 compatibility 35 | 36 | ## rest-firebase 1.0.0 -- 2014-12-09 37 | 38 | ### Enhancement 39 | 40 | * Encode query in JSON to make using [Firebase queries][] easy. 41 | * Introduced `max_retries`, `retry_exceptions`, and `error_callback` from 42 | latest rest-core (3.5.0+). See README.md for detail. 43 | 44 | [Firebase queries]: https://www.firebase.com/docs/rest/guide/retrieving-data.html#section-rest-queries 45 | 46 | ### Internal Enhancement 47 | 48 | * Encode payload in JSON with middleware from rest-core 49 | 50 | ## rest-firebase 0.9.5 -- 2014-11-07 51 | 52 | * Base64url encoded JWT would no longer contain any newlines. 53 | 54 | ## rest-firebase 0.9.4 -- 2014-09-01 55 | 56 | * Should really properly refresh the auth (query) 57 | * From now on you're not allowed to change the value of query. 58 | 59 | ## rest-firebase 0.9.3 -- 2014-08-25 60 | 61 | * Adopted rest-core 3.3.0 62 | * Introduce `RestFirebase#auth_ttl` to setup when to refresh the auth token. 63 | Default to 23 hours (82800 seconds) 64 | * Properly refresh the auth token by resetting `RestFirebase#iat`. 65 | 66 | ## rest-firebase 0.9.2 -- 2014-08-06 67 | 68 | * Now it would auto-refresh auth if it's also expired (>= 23 hours) 69 | 70 | ## rest-firebase 0.9.1 -- 2014-06-28 71 | 72 | * Now it would properly send JSON payload and headers. 73 | 74 | ## rest-firebase 0.9.0 -- 2014-05-13 75 | 76 | * Birthday! 77 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | 2 | source 'https://rubygems.org/' 3 | 4 | gemspec 5 | 6 | # this is for travis-ci 7 | gem 'rest-core', :path => 'rest-core' if 8 | File.exist?("#{File.dirname(File.expand_path(__FILE__))}/rest-core/Gemfile") 9 | 10 | gem 'rake' 11 | gem 'pork' 12 | gem 'muack' 13 | gem 'webmock' 14 | 15 | gem 'json' 16 | gem 'json_pure' 17 | gem 'multi_json' 18 | 19 | gem 'rack' 20 | 21 | gem 'simplecov', :require => false if ENV['COV'] 22 | gem 'coveralls', :require => false if ENV['CI'] 23 | 24 | platforms :ruby do 25 | gem 'yajl-ruby' 26 | end 27 | 28 | platforms :rbx do 29 | gem 'rubysl-weakref' # used in rest-core 30 | gem 'rubysl-singleton' # used in rake 31 | gem 'rubysl-rexml' # used in crack used in webmock 32 | gem 'rubysl-bigdecimal' # used in crack used in webmock 33 | gem 'rubysl-test-unit' # used in activesupport 34 | gem 'rubysl-enumerator' # used in activesupport 35 | gem 'rubysl-benchmark' # used in activesupport 36 | gem 'racc' # used in journey used in actionpack 37 | end 38 | 39 | platforms :jruby do 40 | gem 'jruby-openssl' 41 | end 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rest-firebase [![Build Status](https://secure.travis-ci.org/CodementorIO/rest-firebase.png?branch=master)](http://travis-ci.org/CodementorIO/rest-firebase) [![Coverage Status](https://coveralls.io/repos/github/CodementorIO/rest-firebase/badge.png)](https://coveralls.io/github/CodementorIO/rest-firebase) [![Join the chat at https://gitter.im/CodementorIO/rest-firebase](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/CodementorIO/rest-firebase) 2 | 3 | by [Codementor][] 4 | 5 | [Codementor]: https://www.codementor.io/ 6 | 7 | ## LINKS: 8 | 9 | * [github](https://github.com/CodementorIO/rest-firebase) 10 | * [rubygems](https://rubygems.org/gems/rest-firebase) 11 | * [rdoc](http://rdoc.info/projects/CodementorIO/rest-firebase) 12 | * [issues](https://github.com/CodementorIO/rest-firebase/issues) (feel free to ask for support) 13 | 14 | ## DESCRIPTION: 15 | 16 | Ruby Firebase REST API client built on top of [rest-core][]. 17 | 18 | [rest-core]: https://github.com/godfat/rest-core 19 | 20 | ## FEATURES: 21 | 22 | * Concurrent requests 23 | * Streaming requests 24 | 25 | ## REQUIREMENTS: 26 | 27 | ### Mandatory: 28 | 29 | * Tested with MRI (official CRuby) and JRuby. 30 | * gem [rest-core][] 31 | * gem [httpclient][] 32 | * gem [mime-types][] 33 | * gem [timers][] 34 | 35 | [httpclient]: https://github.com/nahi/httpclient 36 | [mime-types]: https://github.com/halostatue/mime-types 37 | [timers]: https://github.com/celluloid/timers 38 | 39 | ### Optional: 40 | 41 | * gem json or yajl-ruby, or multi_json 42 | 43 | ## INSTALLATION: 44 | 45 | ``` shell 46 | gem install rest-firebase 47 | ``` 48 | 49 | Or if you want development version, put this in Gemfile: 50 | 51 | ``` ruby 52 | gem 'rest-firebase', :git => 'git://github.com/CodementorIO/rest-firebase.git', 53 | :submodules => true 54 | ``` 55 | 56 | ## SYNOPSIS: 57 | 58 | Check out Firebase's 59 | [REST API documentation](https://www.firebase.com/docs/rest-api.html) 60 | for a complete reference. 61 | 62 | ``` ruby 63 | require 'rest-firebase' 64 | 65 | f = RestFirebase.new :site => 'https://SampleChat.firebaseIO-demo.com/', 66 | :secret => 'secret', 67 | :d => {:auth_data => 'something'}, 68 | :log_method => method(:puts), 69 | # `timeout` in seconds 70 | :timeout => 10, 71 | # `max_retries` upon failures. Default is: `0` 72 | :max_retries => 3, 73 | # `retry_exceptions` for which exceptions should retry 74 | # Default is: `[IOError, SystemCallError]` 75 | :retry_exceptions => 76 | [IOError, SystemCallError, Timeout::Error], 77 | # `error_callback` would get called each time there's 78 | # an exception. Useful for monitoring and logging. 79 | :error_callback => method(:p), 80 | # `auth_ttl` describes when we should refresh the auth 81 | # token. Set it to `false` to disable auto-refreshing. 82 | # The default is 23 hours. 83 | :auth_ttl => 82800, 84 | # `auth` is the auth token from Firebase. Leave it alone 85 | # to auto-generate. Set it to `false` to disable it. 86 | :auth => false # Ignore auth for this example! 87 | 88 | @reconnect = true 89 | 90 | # Streaming over 'users/tom' 91 | es = f.event_source('users/tom') 92 | es.onopen { |sock| p sock } # Called when connected 93 | es.onmessage{ |event, data, sock| p event, data } # Called for each message 94 | es.onerror { |error, sock| p error } # Called whenever there's an error 95 | # Extra: If we return true in onreconnect callback, it would automatically 96 | # reconnect the node for us if disconnected. 97 | es.onreconnect{ |error, sock| p error; @reconnect } 98 | 99 | # Start making the request 100 | es.start 101 | 102 | # Try to close the connection and see it reconnects automatically 103 | es.close 104 | 105 | # Update users/tom.json 106 | p f.put('users/tom', :some => 'data') 107 | p f.post('users/tom', :some => 'other') 108 | p f.get('users/tom') 109 | p f.delete('users/tom') 110 | 111 | # With Firebase queries (it would encode query in JSON for you) 112 | p f.get('users/tom', :orderBy => '$key', :limitToFirst => 1) 113 | 114 | # Need to tell onreconnect stops reconnecting, or even if we close 115 | # the connection manually, it would still try to reconnect again. 116 | @reconnect = false 117 | 118 | # Close the connection to gracefully shut it down. 119 | es.close 120 | 121 | # Refresh the auth by resetting it 122 | f.auth = nil 123 | ``` 124 | 125 | ## Concurrent HTTP Requests: 126 | 127 | Inherited from [rest-core][], you can do concurrent requests quite easily. 128 | Here's a very quick example of making two API calls at the same time. 129 | 130 | ``` ruby 131 | require 'rest-firebase' 132 | firebase = RestFirebase.new(:log_method => method(:puts)) 133 | puts "httpclient with threads doing concurrent requests" 134 | a = [firebase.get('users/tom'), firebase.get('users/mom')] 135 | puts "It's not blocking... but doing concurrent requests underneath" 136 | p a.map{ |r| r['name'] } # here we want the values, so it blocks here 137 | puts "DONE" 138 | ``` 139 | 140 | If you prefer callback based solution, this would also work: 141 | 142 | ``` ruby 143 | require 'rest-firebase' 144 | firebase = RestFirebase.new(:log_method => method(:puts)) 145 | puts "callback also works" 146 | firebase.get('users/tom') do |r| 147 | p r['name'] 148 | end 149 | puts "It's not blocking... but doing concurrent requests underneath" 150 | firebase.wait # we block here to wait for the request done 151 | puts "DONE" 152 | ``` 153 | 154 | For a detailed explanation, see: 155 | [Advanced Concurrent HTTP Requests -- Embrace the Future][future] 156 | 157 | [future]: https://github.com/godfat/rest-core#advanced-concurrent-http-requests----embrace-the-future 158 | 159 | ### Thread Pool / Connection Pool 160 | 161 | Underneath, rest-core would spawn a thread for each request, freeing you 162 | from blocking. However, occasionally we would not want this behaviour, 163 | giving that we might have limited resource and cannot maximize performance. 164 | 165 | For example, maybe we could not afford so many threads running concurrently, 166 | or the target server cannot accept so many concurrent connections. In those 167 | cases, we would want to have limited concurrent threads or connections. 168 | 169 | ``` ruby 170 | RestFirebase.pool_size = 10 171 | RestFirebase.pool_idle_time = 60 172 | ``` 173 | 174 | This could set the thread pool size to 10, having a maximum of 10 threads 175 | running together, growing from requests. Each threads idled more than 60 176 | seconds would be shut down automatically. 177 | 178 | Note that `pool_size` should at least be larger than 4, or it might be 179 | very likely to have _deadlock_ if you're using nested callbacks and having 180 | a large number of concurrent calls. 181 | 182 | Also, setting `pool_size` to `-1` would mean we want to make blocking 183 | requests, without spawning any threads. This might be useful for debugging. 184 | 185 | ### Gracefully shutdown 186 | 187 | To shutdown gracefully, consider shutdown the thread pool (if we're using it), 188 | and wait for all requests for a given client. For example: 189 | 190 | ``` ruby 191 | RestFirebase.shutdown 192 | ``` 193 | 194 | We could put them in `at_exit` callback like this: 195 | 196 | ``` ruby 197 | at_exit do 198 | RestFirebase.shutdown 199 | end 200 | ``` 201 | 202 | If you're using unicorn, you probably want to put that in the config. 203 | 204 | ## Powered sites: 205 | 206 | * [Codementor][] 207 | 208 | ## CHANGES: 209 | 210 | * [CHANGES](CHANGES.md) 211 | 212 | ## CONTRIBUTORS: 213 | 214 | * Lin Jen-Shin (@godfat) 215 | * Yoshihiro Ibayashi (@chanibarin) 216 | 217 | ## LICENSE: 218 | 219 | Apache License 2.0 (Apache-2.0) 220 | 221 | Copyright (c) 2014-2019, Codementor 222 | 223 | Licensed under the Apache License, Version 2.0 (the "License"); 224 | you may not use this file except in compliance with the License. 225 | You may obtain a copy of the License at 226 | 227 | 228 | 229 | Unless required by applicable law or agreed to in writing, software 230 | distributed under the License is distributed on an "AS IS" BASIS, 231 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 232 | See the License for the specific language governing permissions and 233 | limitations under the License. 234 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | 2 | begin 3 | require "#{__dir__}/task/gemgem" 4 | rescue LoadError 5 | sh 'git submodule update --init --recursive' 6 | exec Gem.ruby, '-S', $PROGRAM_NAME, *ARGV 7 | end 8 | 9 | Gemgem.init(__dir__, :submodules => 10 | %w[rest-core 11 | rest-core/rest-builder 12 | rest-core/rest-builder/promise_pool]) do |s| 13 | s.name = 'rest-firebase' 14 | s.version = '1.1.0' 15 | s.homepage = 'https://github.com/CodementorIO/rest-firebase' 16 | 17 | s.authors = ['Codementor', 'Lin Jen-Shin (godfat)'] 18 | s.email = ['help@codementor.io'] 19 | 20 | s.add_runtime_dependency('rest-core', '>=4.0.0') 21 | end 22 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | 2 | # Codementor introduces you a new Firebase client for Ruby 3 | 4 | ## Why we pick Firebase 5 | 6 | Here at Codementor we implemented all the realtime facilities with 7 | [Firebase][], which is a great tool and service for realtime communication, 8 | especially for their JavaScript library which could handle all those edge 9 | cases like whenever the clients disconnected unexpectedly, how we could 10 | process the data offline and when we have a chance to reconnect, reconnect 11 | and resend the offline data, etc, which are definitely common enough and we 12 | shall not ignore them. 13 | 14 | [Firebase]: https://www.firebase.com/ 15 | 16 | ## Why we need a Firebase client for Ruby 17 | 18 | However, our server is written in Ruby, and we definitely need someway to let 19 | the server communicate with the clients (browsers). For example, whenever 20 | we want to programmatically broadcast some messages to certain users, it 21 | would be much easier to do this from the server. Picking a Firebase client 22 | for Ruby would be the most straightforward choice. 23 | 24 | ## Existing Firebase client for Ruby did not fit our need 25 | 26 | Unfortunately, eventually we realized that the existing Firebase client for 27 | Ruby, namely [firebase-ruby][], did not fit our need. The main reason is that 28 | it did not support the [streaming feature from Firebase][streaming], which is 29 | extremely important whenever we want the clients periodically notify the 30 | server, (e.g. online presence) since the server needs to know the status in 31 | order to do some other stuffs underneath in realtime. We could probably 32 | implement this on our server, but why not just use Firebase whenever it's 33 | already implemented, and we're using it? 34 | 35 | [firebase-ruby]: https://github.com/oscardelben/firebase-ruby 36 | [streaming]: https://www.firebase.com/docs/rest-api.html#streaming-from-the-rest-api 37 | 38 | ## [rest-firebase][] 39 | 40 | Therefore we implemented our own Firebase client for Ruby, that is 41 | [rest-firebase][]. It was built on top of [rest-core][], thus it has all 42 | the advantages from rest-core, just like firebase-ruby was built on top 43 | of [typhoeus][]. The highlights for rest-firebase are: 44 | 45 | * Concurrent/asynchronous requests 46 | * Streaming requests 47 | * Generate Firebase JWT for you (auto-refresh is WIP) 48 | 49 | [rest-firebase]: https://github.com/CodementorIO/rest-firebase 50 | [rest-core]: https://github.com/godfat/rest-core 51 | [typhoeus]: https://github.com/typhoeus/typhoeus 52 | 53 | ### Concurrent/asynchronous requests 54 | 55 | At times we want to notify two users at the same time, instead of preparing 56 | two requests and wait for two requests to be done, we could simply do this: 57 | (not a working example, just try to demonstrate, see [README.md][] for 58 | working example) 59 | 60 | ``` ruby 61 | f = RestFirebase.new 62 | f.put("users/#{a.id}", :message => 'Hi') 63 | f.put("users/#{b.id}", :message => 'Oh') 64 | ``` 65 | 66 | All requests are non-blocking, and it would only block when we try to look at 67 | the response. Therefore the above requests would be processed concurrently and 68 | asynchronously. To learn more about this, check [Concurrent HTTP Requests][]. 69 | 70 | Also, consequently, if you're not waiting for the requests to be done 71 | somewhere, you might want to wait `at_exit` to make sure all 72 | requests are properly done like this: 73 | 74 | ``` ruby 75 | at_exit do 76 | RestFirebase.shutdown 77 | end 78 | ``` 79 | 80 | Which would also shutdown the [thread pool][] if you're using it. 81 | 82 | [README.md]: https://github.com/CodementorIO/rest-firebase/blob/master/README.md 83 | [Concurrent HTTP Requests]: https://github.com/CodementorIO/rest-firebase/blob/master/README.md#concurrent-http-requests 84 | [thread pool]: https://github.com/godfat/rest-core#thread-pool--connection-pool 85 | 86 | ### Streaming requests 87 | 88 | To receive the online presence events, we have a specialized daemon to listen 89 | on the presence node from Firebase. Something like below: 90 | 91 | ``` ruby 92 | es = RestFirebase.new.event_source('presence') 93 | es.onerror do |error| 94 | Codementor.handle_error(error) unless error.kind_of?(EOFError) 95 | end 96 | 97 | es.onreconnect do 98 | firebase.auth = nil # refresh auth 99 | !!@start # don't reconnect if we're closing 100 | end 101 | 102 | es.onmessage do |event, data| 103 | next unless event == 'put' 104 | next unless username = data['path'][%r{^/(\w+)/web$}, 1] 105 | onpresence(username, data['data']) 106 | end 107 | 108 | es.start 109 | sleep(1) while @start 110 | 111 | es.close 112 | ``` 113 | 114 | `onpresence` is the one doing our business logic. 115 | 116 | ### Generate Firebase JWT for you (auto-refresh is WIP) 117 | 118 | We could use Firebase JWT instead of our secret in order to make authorized 119 | requests. This would be much secure than simply use the secret, which would 120 | never expire unless we explicitly ask for. Checkout 121 | [Authenticating Your Server][] for more detail. [rest-firebase][] could 122 | generate one for you automatically by passing your secret to it like this: 123 | 124 | ``` ruby 125 | f = RestFirebase.new :secret => 'secret', 126 | :d => {:auth_data => 'something'} 127 | f.get('presence') # => attach JWT for auth in the request automatically 128 | f.auth # => the JWT 129 | f.auth = nil # => remove old JWT 130 | f.auth # => generate a fresh new JWT 131 | ``` 132 | 133 | Read the above document for what `:d` means here. Note that this JWT 134 | would expire after 24 hours. Every time you initialize a new `RestFirebase` 135 | it would generate a fresh new JWT, but if you want to keep using the same 136 | instance, you would probably need to refresh the JWT by yourselves, just like 137 | what we did when we tried to reconnect it in the streaming example. 138 | 139 | [Authenticating Your Server]: https://www.firebase.com/docs/security/custom-login.html#authenticating-your-server 140 | 141 | ## Summary 142 | 143 | In order to take the full advantage of using Firebase with Ruby, we introduce 144 | you [rest-firebase][], which highlights: 145 | 146 | * Concurrent/asynchronous requests 147 | * Streaming requests 148 | * Generate Firebase JWT for you (auto-refresh is WIP) 149 | 150 | Please feel free to try it and use it. It's released under Apache License 2.0. 151 | -------------------------------------------------------------------------------- /example/daemon.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-firebase' 3 | 4 | es = RestFirebase.new(:auth => false). 5 | event_source('https://SampleChat.firebaseIO-demo.com/') 6 | 7 | es.onerror do |error| 8 | puts "ERROR: #{error}" 9 | end 10 | 11 | es.onreconnect do 12 | !!@start # always reconnect unless stopping 13 | end 14 | 15 | es.onmessage do |event, data| 16 | puts "EVENT: #{event}, DATA: #{data}" 17 | end 18 | 19 | puts "Starting..." 20 | @start = true 21 | es.start 22 | 23 | rd, wr = IO.pipe 24 | 25 | Signal.trap('INT') do # intercept ctrl-c 26 | puts "Stopping..." 27 | @start = false # stop reconnecting 28 | es.close # close socket 29 | es.wait # wait for shutting down 30 | wr.puts # unblock main thread 31 | end 32 | 33 | rd.gets # main thread blocks here 34 | 35 | # Now try: 36 | # curl -X POST -d '{"message": "Hi!"}' https://SampleChat.firebaseIO-demo.com/godfat.json 37 | -------------------------------------------------------------------------------- /lib/rest-firebase.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-core' 3 | require 'rest-core/util/json' 4 | require 'rest-core/util/hmac' 5 | 6 | # https://www.firebase.com/docs/security/custom-login.html 7 | # https://www.firebase.com/docs/rest-api.html 8 | # https://www.firebase.com/docs/rest/guide/retrieving-data.html#section-rest-queries 9 | RestFirebase = 10 | RestCore::Builder.client(:d, :secret, :auth, :auth_ttl, :iat) do 11 | use RestCore::DefaultSite , 'https://SampleChat.firebaseIO-demo.com/' 12 | use RestCore::DefaultHeaders, {'Accept' => 'application/json', 13 | 'Content-Type' => 'application/json'} 14 | use RestCore::DefaultQuery , nil 15 | use RestCore::JsonRequest , true 16 | 17 | use RestCore::Retry , 0, RestCore::Retry::DefaultRetryExceptions 18 | use RestCore::Timeout , 10 19 | use RestCore::FollowRedirect, 5 20 | use RestCore::ErrorHandler , lambda{|env| RestFirebase::Error.call(env)} 21 | use RestCore::ErrorDetectorHttp 22 | use RestCore::JsonResponse , true 23 | use RestCore::CommonLogger , nil 24 | use RestCore::Cache , nil, 600 25 | end 26 | 27 | 28 | require 'rest-firebase/error' 29 | require 'rest-firebase/client' 30 | 31 | class RestFirebase 32 | include RestFirebase::Client 33 | self.event_source_class = EventSource 34 | const_get(:Struct).send(:remove_method, :query=) 35 | end 36 | -------------------------------------------------------------------------------- /lib/rest-firebase/client.rb: -------------------------------------------------------------------------------- 1 | 2 | module RestFirebase::Client 3 | class EventSource < RestCore::EventSource 4 | def onmessage event=nil, data=nil, sock=nil 5 | if event 6 | super(event, RestCore::Json.decode(data), sock) 7 | else 8 | super 9 | end 10 | end 11 | end 12 | 13 | def request env, a=app 14 | check_auth 15 | query = env[RestCore::REQUEST_QUERY].inject({}) do |q, (k, v)| 16 | q[k] = RestCore::Json.encode(v) 17 | q 18 | end 19 | super(env.merge(RestCore::REQUEST_PATH => 20 | "#{env[RestCore::REQUEST_PATH]}.json", 21 | RestCore::REQUEST_QUERY => query), a) 22 | end 23 | 24 | def generate_auth opts={} 25 | raise RestFirebase::Error::ClientError.new( 26 | "Please set your secret") unless secret 27 | 28 | self.iat = nil 29 | header = {:typ => 'JWT', :alg => 'HS256'} 30 | claims = {:v => 0, :iat => iat, :d => d}.merge(opts) 31 | # http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-26 32 | input = [header, claims].map{ |d| base64url(RestCore::Json.encode(d)) }. 33 | join('.') 34 | # http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-20 35 | "#{input}.#{base64url(RestCore::Hmac.sha256(secret, input))}" 36 | end 37 | 38 | def query 39 | {:auth => auth} 40 | end 41 | 42 | private 43 | def base64url str; [str].pack('m0').tr('+/', '-_'); end 44 | def default_auth ; generate_auth ; end 45 | def default_auth_ttl; 82800 ; end 46 | def default_iat ; Time.now.to_i ; end 47 | 48 | def check_auth 49 | self.auth = nil if auth_ttl && Time.now.to_i - iat > auth_ttl 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/rest-firebase/error.rb: -------------------------------------------------------------------------------- 1 | 2 | class RestFirebase::Error < RestCore::Error 3 | class ServerError < RestFirebase::Error; end 4 | class ClientError < RestCore::Error; end 5 | 6 | class BadRequest < RestFirebase::Error; end 7 | class Unauthorized < RestFirebase::Error; end 8 | class Forbidden < RestFirebase::Error; end 9 | class NotFound < RestFirebase::Error; end 10 | class NotAcceptable < RestFirebase::Error; end 11 | class ExpectationFailed < RestFirebase::Error; end 12 | 13 | class InternalServerError < RestFirebase::Error::ServerError; end 14 | class BadGateway < RestFirebase::Error::ServerError; end 15 | class ServiceUnavailable < RestFirebase::Error::ServerError; end 16 | 17 | attr_reader :error, :code, :url 18 | def initialize error, code, url='' 19 | @error, @code, @url = error, code, url 20 | super("[#{code}] #{error.inspect} from #{url}") 21 | end 22 | 23 | def self.call env 24 | error, code, url = env[RestCore::RESPONSE_BODY], 25 | env[RestCore::RESPONSE_STATUS], 26 | env[RestCore::REQUEST_URI] 27 | return new(error, code, url) unless error.kind_of?(Hash) 28 | case code 29 | when 400; BadRequest 30 | when 401; Unauthorized 31 | when 403; Forbidden 32 | when 404; NotFound 33 | when 406; NotAcceptable 34 | when 417; ExpectationFailed 35 | when 500; InternalServerError 36 | when 502; BadGateway 37 | when 503; ServiceUnavailable 38 | else ; self 39 | end.new(error, code, url) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /rest-firebase.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # stub: rest-firebase 1.1.0 ruby lib 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "rest-firebase".freeze 6 | s.version = "1.1.0" 7 | 8 | s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= 9 | s.require_paths = ["lib".freeze] 10 | s.authors = [ 11 | "Codementor".freeze, 12 | "Lin Jen-Shin (godfat)".freeze] 13 | s.date = "2018-12-26" 14 | s.description = "Ruby Firebase REST API client built on top of [rest-core][].\n\n[rest-core]: https://github.com/godfat/rest-core".freeze 15 | s.email = ["help@codementor.io".freeze] 16 | s.files = [ 17 | ".gitignore".freeze, 18 | ".gitmodules".freeze, 19 | ".travis.yml".freeze, 20 | "CHANGES.md".freeze, 21 | "Gemfile".freeze, 22 | "LICENSE".freeze, 23 | "README.md".freeze, 24 | "Rakefile".freeze, 25 | "TODO.md".freeze, 26 | "doc/intro.md".freeze, 27 | "example/daemon.rb".freeze, 28 | "lib/rest-firebase.rb".freeze, 29 | "lib/rest-firebase/client.rb".freeze, 30 | "lib/rest-firebase/error.rb".freeze, 31 | "pkey.pem".freeze, 32 | "rest-firebase.gemspec".freeze, 33 | "task/README.md".freeze, 34 | "task/gemgem.rb".freeze, 35 | "test/test_api.rb".freeze] 36 | s.homepage = "https://github.com/CodementorIO/rest-firebase".freeze 37 | s.licenses = ["Apache-2.0".freeze] 38 | s.rubygems_version = "3.0.1".freeze 39 | s.summary = "Ruby Firebase REST API client built on top of [rest-core][].".freeze 40 | s.test_files = ["test/test_api.rb".freeze] 41 | 42 | if s.respond_to? :specification_version then 43 | s.specification_version = 4 44 | 45 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 46 | s.add_runtime_dependency(%q.freeze, [">= 4.0.0"]) 47 | else 48 | s.add_dependency(%q.freeze, [">= 4.0.0"]) 49 | end 50 | else 51 | s.add_dependency(%q.freeze, [">= 4.0.0"]) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/test_api.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rest-firebase' 3 | require 'rest-builder/test' 4 | 5 | Pork.protected_exceptions << WebMock::NetConnectNotAllowedError 6 | 7 | Pork::API.describe RestFirebase do 8 | before do 9 | stub_select_for_stringio 10 | stub(Time).now{ Time.at(86400) } 11 | end 12 | 13 | after do 14 | WebMock.reset! 15 | Muack.verify 16 | end 17 | 18 | path = 'https://a.json?auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ2IjowLCJpYXQiOjg2NDAwLCJkIjpudWxsfQ%3D%3D.SSmw2fUYiQFyYlsFV8WmyQsOCWJ6yvC7aw3bRpwQOYo%3D' 19 | 20 | json = '{"status":"ok"}' 21 | rbon = {'status' => 'ok'} 22 | 23 | def firebase 24 | @firebase ||= RestFirebase.new(:secret => 'nnf') 25 | end 26 | 27 | would 'get true' do 28 | stub_request(:get, path).to_return(:body => 'true') 29 | firebase.get('https://a').should.eq true 30 | end 31 | 32 | would 'get true with callback' do 33 | stub_request(:get, path).to_return(:body => 'true') 34 | firebase.get('https://a') do |r| 35 | r.should.eq true 36 | end.wait 37 | end 38 | 39 | would 'get with query' do 40 | stub_request(:get, "#{path}&orderBy=%22date%22&limitToFirst=1"). 41 | to_return(:body => json) 42 | firebase.get('https://a', :orderBy => 'date', :limitToFirst => 1). 43 | should.eq rbon 44 | end 45 | 46 | would 'put {"status":"ok"}' do 47 | stub_request(:put, path).with(:body => json).to_return(:body => json) 48 | firebase.put('https://a', rbon).should.eq rbon 49 | end 50 | 51 | would 'have no payload for delete' do 52 | stub_request(:delete, path).with(:body => nil).to_return(:body => json) 53 | firebase.delete('https://a').should.eq rbon 54 | end 55 | 56 | would 'parse event source' do 57 | stub_request(:get, path).to_return(:body => <<-SSE) 58 | event: put 59 | data: {} 60 | 61 | event: keep-alive 62 | data: null 63 | 64 | event: invalid 65 | data: invalid 66 | SSE 67 | m = [{'event' => 'put' , 'data' => {}}, 68 | {'event' => 'keep-alive', 'data' => nil}] 69 | es = firebase.event_source('https://a') 70 | es.should.kind_of? RestFirebase::Client::EventSource 71 | es.onmessage do |event, data| 72 | {'event' => event, 'data' => data}.should.eq m.shift 73 | end.onerror do |error| 74 | error.should.kind_of? RC::Json::ParseError 75 | end.start.wait 76 | m.should.empty? 77 | end 78 | 79 | would 'refresh token' do 80 | firebase # initialize http-client first (it's using Time.now too) 81 | mock(Time).now{ Time.at(0) } 82 | auth, query = firebase.auth, firebase.query 83 | query[:auth].should.eq auth 84 | Muack.verify(Time) 85 | stub(Time).now{ Time.at(86400) } 86 | 87 | stub_request(:get, path).to_return(:body => 'true') 88 | firebase.get('https://a').should.eq true 89 | firebase.auth .should.not.eq auth 90 | firebase.query.should.not.eq query 91 | end 92 | 93 | would 'not double encode json upon retrying' do 94 | stub_request(:post, path). 95 | to_return(:body => '{}', :status => 500).times(1).then. 96 | to_return(:body => '[]', :status => 200).times(1). 97 | with(:body => '{"is":"ok"}') 98 | 99 | firebase.retry_exceptions = [TrueClass] 100 | firebase.max_retries = 1 101 | firebase.error_handler = false 102 | expect(firebase.post(path, :is => :ok)).eq([]) 103 | end 104 | 105 | define_method :check do |status, klass| 106 | stub_request(:delete, path).to_return( 107 | :body => '{}', :status => status) 108 | 109 | lambda{ firebase.delete('https://a').tap{} }.should.raise(klass) 110 | 111 | WebMock.reset! 112 | end 113 | 114 | would 'raise exception when encountering error' do 115 | [400, 401, 402, 403, 404, 406, 417].each do |status| 116 | check(status, RestFirebase::Error) 117 | end 118 | [500, 502, 503].each do |status| 119 | check(status, RestFirebase::Error::ServerError) 120 | end 121 | end 122 | end 123 | --------------------------------------------------------------------------------