├── .document ├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── firebase.gemspec ├── lib ├── firebase.rb └── firebase │ ├── response.rb │ ├── server_value.rb │ └── version.rb └── spec ├── firebase_spec.rb └── spec_helper.rb /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rcov generated 2 | coverage 3 | coverage.data 4 | 5 | # rdoc generated 6 | rdoc 7 | 8 | # yard generated 9 | doc 10 | .yardoc 11 | 12 | # bundler 13 | .bundle 14 | 15 | # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: 16 | # 17 | # * Create a file at ~/.gitignore 18 | # * Include files you want ignored 19 | # * Run: git config --global core.excludesfile ~/.gitignore 20 | # 21 | # After doing this, these files will be ignored in all your git projects, 22 | # saving you from having to 'pollute' every project you touch with them 23 | # 24 | # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line) 25 | # 26 | # For MacOS: 27 | # 28 | #.DS_Store 29 | 30 | # For TextMate 31 | #*.tmproj 32 | #tmtags 33 | 34 | # For emacs: 35 | #*~ 36 | #\#* 37 | #.\#* 38 | 39 | # For vim: 40 | #*.swp 41 | 42 | # For redcar: 43 | #.redcar 44 | 45 | # For rubinius: 46 | #*.rbc 47 | *.swp 48 | 49 | Gemfile.lock 50 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.3 5 | - 2.4 6 | - 2.5 7 | - 2.6 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2.8 4 | 5 | * Fix [auth token expiration](https://github.com/oscardelben/firebase-ruby/pull/84) on longer lived Firebase objects. 6 | 7 | ## 0.2.7 8 | 9 | * Support newer Firebase [authentication method](https://github.com/oscardelben/firebase-ruby/pull/81) 10 | 11 | ## 0.2.5 12 | 13 | * [Refactor to remove Request proxy class, expose http client](https://github.com/oscardelben/firebase-ruby/commit/138b1e1461ff33da506b0d7992b42e3544be9cf1) 14 | 15 | ## 0.2.4 16 | 17 | * Add support for server timestamp. 18 | 19 | ## 0.2.3 20 | 21 | * Switch from problematic `typhoeus` client to `HTTPClient` 22 | * File permissions issue fix 23 | * Follow redirect headers by default 24 | 25 | ## 0.2.2 26 | 27 | * Update dependencies 28 | 29 | ## 0.2.1 30 | 31 | * Fix auth parse exception 32 | 33 | ## 0.2.0 34 | 35 | * You can now pass query options to get/push/set, etc. 36 | * The old syntax no longer works. You now need to create instance variables of Firebase::Client.new(...) 37 | 38 | ## 0.1.6 39 | 40 | * You can now create instances of Firebase. The old syntax still works but will be removed in version 0,2. - @wannabefro 41 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Oscar Del Ben 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # firebase [![Build Status](https://travis-ci.org/oscardelben/firebase-ruby.svg?branch=master)](https://travis-ci.org/oscardelben/firebase-ruby) [![Gem Version](https://badge.fury.io/rb/firebase.svg)](https://rubygems.org/gems/firebase) 2 | 3 | 4 | Ruby wrapper for the [Firebase REST API](https://firebase.google.com/docs/reference/rest/database/). 5 | 6 | Changes are sent to all subscribed clients automatically, so you can 7 | update your clients **in realtime from the backend**. 8 | 9 | See a [video demo](https://vimeo.com/41494336?utm_source=internal&utm_medium=email&utm_content=cliptranscoded&utm_campaign=adminclip) of what's possible. 10 | 11 | ## Installation 12 | 13 | ``` 14 | gem install firebase 15 | ``` 16 | ## Usage 17 | 18 | ```ruby 19 | base_uri = 'https://.firebaseio.com/' 20 | 21 | firebase = Firebase::Client.new(base_uri) 22 | 23 | response = firebase.push("todos", { :name => 'Pick the milk', :'.priority' => 1 }) 24 | response.success? # => true 25 | response.code # => 200 26 | response.body # => { 'name' => "-INOQPH-aV_psbk3ZXEX" } 27 | response.raw_body # => '{"name":"-INOQPH-aV_psbk3ZXEX"}' 28 | ``` 29 | 30 | ### Authentication 31 | If you have a read-only namespace, you need to authenticate your Firebase client. `firebase-ruby` will attempt to determine if you are using the old or new [authentication method](https://firebase.google.com/docs/database/rest/auth) by whether your auth string is a valid JSON string or not. 32 | 33 | #### Using Firebase Database Secret (deprecated) 34 | ``` 35 | # Using Firebase Database Secret (deprecated) 36 | firebase = Firebase::Client.new(base_uri, db_secret) 37 | ``` 38 | 39 | #### Using Firebase Admin SDK private key 40 | Go to the Firebase console and under `Project Settings` -> `Service Accounts` -> `Firebase Admin SDK` click on `GENERATE NEW PRIVATE KEY`. Save the json file and use it like this: 41 | 42 | ```ruby 43 | # Using Firebase Admin SDK private key 44 | private_key_json_string = File.open('/path/to/your/generated/json').read 45 | firebase = Firebase::Client.new(base_uri, private_key_json_string) 46 | ``` 47 | 48 | 49 | You can now pass custom query options to firebase: 50 | 51 | ```ruby 52 | response = firebase.push("todos", :limit => 1) 53 | ``` 54 | 55 | To populate a value with a Firebase server timestamp, you can set `Firebase::ServerValue::TIMESTAMP` as a normal value. This is analogous to passing `Firebase.ServerValue.TIMESTAMP` in the [official JavaScript client](https://www.firebase.com/docs/web/api/servervalue/timestamp.html). 56 | 57 | ```ruby 58 | response = firebase.push("todos", { 59 | :name => 'Pick the milk', 60 | :created => Firebase::ServerValue::TIMESTAMP 61 | }) 62 | ``` 63 | 64 | To update multiple values that are not direct descendants, supply their paths as keys in the payload to update: 65 | 66 | ```ruby 67 | # note the empty path string here as the first argument 68 | firebase.update('', { 69 | "users/posts/#{postID}" => true, 70 | "posts/#{postID}" => text 71 | }) 72 | ``` 73 | 74 | So far, supported methods are: 75 | 76 | ```ruby 77 | set(path, data, query_options) 78 | get(path, query_options) 79 | push(path, data, query_options) 80 | delete(path, query_options) 81 | update(path, data, query_options) 82 | ``` 83 | 84 | ### Configuring HTTP options 85 | 86 | [httpclient](https://github.com/nahi/httpclient) is used under the covers to make HTTP requests. 87 | You may find yourself wanting to tweak the timeout settings. By default, `httpclient` uses 88 | some [sane defaults](https://github.com/nahi/httpclient/blob/dd322d39d4d11c48f7bbbc05ed6273ac912d3e3b/lib/httpclient/session.rb#L138), 89 | but it is quite easy to change them by modifying the `request` object directly: 90 | 91 | ```ruby 92 | firebase = Firebase::Client.new(base_uri) 93 | # firebase.request is a regular httpclient object 94 | firebase.request.connect_timeout = 30 95 | ``` 96 | 97 | More information about Firebase and the Firebase API is available at the 98 | [official website](http://www.firebase.com/). 99 | 100 | ## Copyright 101 | 102 | Copyright (c) 2013 Oscar Del Ben. See LICENSE.txt for 103 | further details. 104 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | begin 6 | Bundler.setup(:default, :development) 7 | rescue Bundler::BundlerError => e 8 | $stderr.puts e.message 9 | $stderr.puts "Run `bundle install` to install missing gems" 10 | exit e.status_code 11 | end 12 | require 'rake' 13 | 14 | require 'rspec/core' 15 | require 'rspec/core/rake_task' 16 | RSpec::Core::RakeTask.new(:spec) do |spec| 17 | spec.pattern = FileList['spec/**/*_spec.rb'] 18 | end 19 | 20 | RSpec::Core::RakeTask.new(:rcov) do |spec| 21 | spec.pattern = 'spec/**/*_spec.rb' 22 | spec.rcov = true 23 | end 24 | 25 | task :default => :spec 26 | 27 | require 'rdoc/task' 28 | Rake::RDocTask.new do |rdoc| 29 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 30 | 31 | rdoc.rdoc_dir = 'rdoc' 32 | rdoc.title = "firebase #{version}" 33 | rdoc.rdoc_files.include('README*') 34 | rdoc.rdoc_files.include('lib/**/*.rb') 35 | end 36 | -------------------------------------------------------------------------------- /firebase.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'firebase/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "firebase" 7 | s.version = Firebase::VERSION 8 | 9 | s.require_paths = ["lib"] 10 | s.authors = ["Oscar Del Ben", "Vincent Woo"] 11 | s.date = "2018-01-28" 12 | s.description = "Firebase wrapper for Ruby" 13 | s.email = "info@oscardelben.com" 14 | s.extra_rdoc_files = [ 15 | "CHANGELOG.md", 16 | "LICENSE.txt", 17 | "README.md" 18 | ] 19 | s.files = [ 20 | "lib/firebase.rb", 21 | "lib/firebase/response.rb", 22 | "lib/firebase/server_value.rb", 23 | "lib/firebase/version.rb" 24 | ] 25 | s.homepage = "https://github.com/oscardelben/firebase-ruby" 26 | s.licenses = ["MIT"] 27 | s.summary = "Firebase wrapper for Ruby" 28 | 29 | s.add_runtime_dependency 'httpclient', '>= 2.5.3' 30 | s.add_runtime_dependency 'json' 31 | s.add_runtime_dependency 'googleauth' 32 | s.add_development_dependency 'rake' 33 | s.add_development_dependency 'rdoc' 34 | s.add_development_dependency 'rspec' 35 | end 36 | 37 | -------------------------------------------------------------------------------- /lib/firebase.rb: -------------------------------------------------------------------------------- 1 | require 'firebase/response' 2 | require 'firebase/server_value' 3 | require 'googleauth' 4 | require 'httpclient' 5 | require 'json' 6 | require 'uri' 7 | 8 | module Firebase 9 | class Client 10 | attr_reader :auth, :request 11 | 12 | def initialize(base_uri, auth=nil, scope=%w(https://www.googleapis.com/auth/firebase.database https://www.googleapis.com/auth/userinfo.email )) 13 | if base_uri !~ URI::regexp(%w(https)) 14 | raise ArgumentError.new('base_uri must be a valid https uri') 15 | end 16 | base_uri += '/' unless base_uri.end_with?('/') 17 | @request = HTTPClient.new({ 18 | :base_url => base_uri, 19 | :default_header => { 20 | 'Content-Type' => 'application/json' 21 | } 22 | }) 23 | if auth && valid_json?(auth) 24 | # Using Admin SDK service account 25 | @credentials = Google::Auth::DefaultCredentials.make_creds( 26 | json_key_io: StringIO.new(auth), 27 | scope: scope 28 | ) 29 | @credentials.apply!(@request.default_header) 30 | @expires_at = @credentials.issued_at + 0.95 * @credentials.expires_in 31 | else 32 | # Using deprecated Database Secret 33 | @secret = auth 34 | end 35 | end 36 | 37 | # Writes and returns the data 38 | # Firebase.set('users/info', { 'name' => 'Oscar' }) => { 'name' => 'Oscar' } 39 | def set(path, data, query={}) 40 | process :put, path, data, query 41 | end 42 | 43 | # Returns the data at path 44 | def get(path, query={}) 45 | process :get, path, nil, query 46 | end 47 | 48 | # Writes the data, returns the key name of the data added 49 | # Firebase.push('users', { 'age' => 18}) => {"name":"-INOQPH-aV_psbk3ZXEX"} 50 | def push(path, data, query={}) 51 | process :post, path, data, query 52 | end 53 | 54 | # Deletes the data at path and returs true 55 | def delete(path, query={}) 56 | process :delete, path, nil, query 57 | end 58 | 59 | # Write the data at path but does not delete ommited children. Returns the data 60 | # Firebase.update('users/info', { 'name' => 'Oscar' }) => { 'name' => 'Oscar' } 61 | def update(path, data, query={}) 62 | process :patch, path, data, query 63 | end 64 | 65 | private 66 | 67 | def process(verb, path, data=nil, query={}) 68 | if path[0] == '/' 69 | raise(ArgumentError.new("Invalid path: #{path}. Path must be relative")) 70 | end 71 | 72 | if @expires_at && Time.now > @expires_at 73 | @credentials.refresh! 74 | @credentials.apply! @request.default_header 75 | @expires_at = @credentials.issued_at + 0.95 * @credentials.expires_in 76 | end 77 | 78 | Firebase::Response.new @request.request(verb, "#{path}.json", { 79 | :body => data.to_json, 80 | :query => (@secret ? { :auth => @secret }.merge(query) : query), 81 | :follow_redirect => true 82 | }) 83 | end 84 | 85 | def valid_json?(json) 86 | begin 87 | JSON.parse(json) 88 | return true 89 | rescue JSON::ParserError 90 | return false 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/firebase/response.rb: -------------------------------------------------------------------------------- 1 | module Firebase 2 | class Response 3 | attr_accessor :response 4 | 5 | def initialize(response) 6 | @response = response 7 | end 8 | 9 | def body 10 | JSON.parse(response.body, :quirks_mode => true) 11 | end 12 | 13 | def raw_body 14 | response.body 15 | end 16 | 17 | def success? 18 | [200, 204].include? response.status 19 | end 20 | 21 | def code 22 | response.status 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/firebase/server_value.rb: -------------------------------------------------------------------------------- 1 | module Firebase 2 | class ServerValue 3 | TIMESTAMP = { '.sv' => 'timestamp' }.freeze 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/firebase/version.rb: -------------------------------------------------------------------------------- 1 | module Firebase 2 | VERSION = '0.2.8'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /spec/firebase_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Firebase" do 4 | let (:data) do 5 | { 'name' => 'Oscar' } 6 | end 7 | 8 | describe "invalid uri" do 9 | it "should raise on http" do 10 | expect{ Firebase::Client.new('http://test.firebaseio.com') }.to raise_error(ArgumentError) 11 | end 12 | 13 | it 'should raise on empty' do 14 | expect{ Firebase::Client.new('') }.to raise_error(ArgumentError) 15 | end 16 | 17 | it "should raise when a nonrelative path is used" do 18 | firebase = Firebase::Client.new('https://test.firebaseio.com') 19 | expect { firebase.get('/path', {}) }.to raise_error(ArgumentError) 20 | end 21 | end 22 | 23 | before do 24 | @firebase = Firebase::Client.new('https://test.firebaseio.com') 25 | end 26 | 27 | describe "set" do 28 | it "writes and returns the data" do 29 | expect(@firebase).to receive(:process).with(:put, 'users/info', data, {}) 30 | @firebase.set('users/info', data) 31 | end 32 | end 33 | 34 | describe "get" do 35 | it "returns the data" do 36 | expect(@firebase).to receive(:process).with(:get, 'users/info', nil, {}) 37 | @firebase.get('users/info') 38 | end 39 | 40 | it "correctly passes custom ordering params" do 41 | params = { 42 | :orderBy => '"$key"', 43 | :startAt => '"A1"' 44 | } 45 | expect(@firebase).to receive(:process).with(:get, 'users/info', nil, params) 46 | @firebase.get('users/info', params) 47 | end 48 | 49 | it "return nil if response body contains 'null'" do 50 | mock_response = double(:body => 'null') 51 | response = Firebase::Response.new(mock_response) 52 | expect { response.body }.to_not raise_error 53 | end 54 | 55 | it "return true if response body contains 'true'" do 56 | mock_response = double(:body => 'true') 57 | response = Firebase::Response.new(mock_response) 58 | expect(response.body).to eq(true) 59 | end 60 | 61 | it "return false if response body contains 'false'" do 62 | mock_response = double(:body => 'false') 63 | response = Firebase::Response.new(mock_response) 64 | expect(response.body).to eq(false) 65 | end 66 | 67 | it "raises JSON::ParserError if response body contains invalid JSON" do 68 | mock_response = double(:body => '{"this is wrong"') 69 | response = Firebase::Response.new(mock_response) 70 | expect { response.body }.to raise_error(JSON::ParserError) 71 | end 72 | end 73 | 74 | describe "push" do 75 | it "writes the data" do 76 | expect(@firebase).to receive(:process).with(:post, 'users', data, {}) 77 | @firebase.push('users', data) 78 | end 79 | end 80 | 81 | describe "delete" do 82 | it "returns true" do 83 | expect(@firebase).to receive(:process).with(:delete, 'users/info', nil, {}) 84 | @firebase.delete('users/info') 85 | end 86 | end 87 | 88 | describe "update" do 89 | it "updates and returns the data" do 90 | expect(@firebase).to receive(:process).with(:patch, 'users/info', data, {}) 91 | @firebase.update('users/info', data) 92 | end 93 | end 94 | 95 | describe "http processing" do 96 | it "sends custom auth query" do 97 | firebase = Firebase::Client.new('https://test.firebaseio.com', 'secret') 98 | expect(firebase.request).to receive(:request).with(:get, "todos.json", { 99 | :body => nil, 100 | :query => {:auth => "secret", :foo => 'bar'}, 101 | :follow_redirect => true 102 | }) 103 | firebase.get('todos', :foo => 'bar') 104 | end 105 | end 106 | 107 | describe "service account auth" do 108 | before do 109 | credential_auth_count = 0 110 | @credentials = double('credentials') 111 | allow(@credentials).to receive(:apply!).with(instance_of(Hash)) do |arg| 112 | credential_auth_count += 1 113 | arg[:authorization] = "Bearer #{credential_auth_count}" 114 | end 115 | allow(@credentials).to receive(:issued_at) { Time.now } 116 | allow(@credentials).to receive(:expires_in) { 3600 } 117 | 118 | expect(Google::Auth::DefaultCredentials).to receive(:make_creds).with( 119 | json_key_io: instance_of(StringIO), 120 | scope: instance_of(Array) 121 | ).and_return(@credentials) 122 | end 123 | 124 | it "sets custom auth header" do 125 | client = Firebase::Client.new('https://test.firebaseio.com/', '{ "private_key": true }') 126 | expect(client.request.default_header).to eql({ 127 | 'Content-Type' => 'application/json', 128 | :authorization => 'Bearer 1' 129 | }) 130 | end 131 | 132 | it "handles token expiry" do 133 | current_time = Time.now 134 | client = Firebase::Client.new('https://test.firebaseio.com/', '{ "private_key": true }') 135 | allow(Time).to receive(:now) { current_time + 3600 } 136 | expect(@credentials).to receive(:refresh!) 137 | client.get 'dummy' 138 | expect(client.request.default_header).to eql({ 139 | 'Content-Type' => 'application/json', 140 | :authorization => 'Bearer 2' 141 | }) 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 3 | require 'rspec' 4 | require 'firebase' 5 | 6 | # Requires supporting files with custom matchers and macros, etc, 7 | # in ./support/ and its subdirectories. 8 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} 9 | 10 | RSpec.configure do |config| 11 | 12 | end 13 | --------------------------------------------------------------------------------