diff --git a/CHANGELOG.md b/CHANGELOG.md index cc050e3..d4df3ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.3.8 +* Major version change as new class added to handle request. +* Classes separated in different files. +* Syntax for Hashes changed from hashrockets to symbol. +* Alias for request methods defined to match HTTP methods +* New class Database defined so that other Firebase services can be added in future and make Client deprecated. +* Rubocop fixes. + ## 0.2.8 * Fix [auth token expiration](https://github.com/oscardelben/firebase-ruby/pull/84) on longer lived Firebase objects. diff --git a/README.md b/README.md index ee90f25..8838bf1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ gem install firebase ```ruby base_uri = 'https://.firebaseio.com/' +firebase = Firebase::Database.new(base_uri) + +or + firebase = Firebase::Client.new(base_uri) response = firebase.push("todos", { :name => 'Pick the milk', :'.priority' => 1 }) @@ -74,10 +78,20 @@ firebase.update('', { So far, supported methods are: ```ruby -set(path, data, query_options) get(path, query_options) + +set(path, data, query_options) +or +put(path, data, query_options) + push(path, data, query_options) +or +post(path, data, query_options) + delete(path, query_options) +or +destroy(path, query_options) + update(path, data, query_options) ``` diff --git a/firebase.gemspec b/firebase.gemspec index 4eb5216..eca0833 100644 --- a/firebase.gemspec +++ b/firebase.gemspec @@ -2,35 +2,25 @@ lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'firebase/version' -Gem::Specification.new do |s| - s.name = "firebase" - s.version = Firebase::VERSION - s.require_paths = ["lib"] - s.authors = ["Oscar Del Ben", "Vincent Woo"] - s.date = "2018-01-28" - s.description = "Firebase wrapper for Ruby" - s.email = "info@oscardelben.com" - s.extra_rdoc_files = [ - "CHANGELOG.md", - "LICENSE.txt", - "README.md" - ] - s.files = [ - "lib/firebase.rb", - "lib/firebase/response.rb", - "lib/firebase/server_value.rb", - "lib/firebase/version.rb" - ] - s.homepage = "http://github.com/oscardelben/firebase-ruby" - s.licenses = ["MIT"] - s.summary = "Firebase wrapper for Ruby" +Gem::Specification.new do |spec| + spec.name = 'firebase' + spec.version = Firebase::VERSION + spec.require_paths = ['lib'] + spec.authors = ['Oscar Del Ben', 'Vincent Woo', 'Edwin Rozario'] + spec.description = 'Firebase wrapper for Ruby' + spec.email = 'info@oscardelben.com' + spec.extra_rdoc_files = %w[CHANGELOG.md LICENSE.txt README.md] + spec.files = `git ls-files lib/`.split($/) + spec.homepage = 'http://github.com/oscardelben/firebase-ruby' + spec.licenses = ['MIT'] + spec.summary = 'Firebase wrapper for Ruby' - s.add_runtime_dependency 'httpclient', '>= 2.5.3' - s.add_runtime_dependency 'json' - s.add_runtime_dependency 'googleauth' - s.add_development_dependency 'rake' - s.add_development_dependency 'rdoc' - s.add_development_dependency 'rspec' + spec.add_runtime_dependency 'httpclient', '>= 2.5.3' + spec.add_runtime_dependency 'json' + spec.add_runtime_dependency 'googleauth' + spec.add_development_dependency 'rake' + spec.add_development_dependency 'rdoc' + spec.add_development_dependency 'rspec' + spec.add_development_dependency 'pry' end - diff --git a/lib/firebase.rb b/lib/firebase.rb index 7314c6c..f7c90b8 100644 --- a/lib/firebase.rb +++ b/lib/firebase.rb @@ -1,94 +1,11 @@ +# frozen_string_literal: true + require 'firebase/response' +require 'firebase/request' require 'firebase/server_value' -require 'googleauth' -require 'httpclient' -require 'json' -require 'uri' - -module Firebase - class Client - attr_reader :auth, :request - - def initialize(base_uri, auth=nil) - if base_uri !~ URI::regexp(%w(https)) - raise ArgumentError.new('base_uri must be a valid https uri') - end - base_uri += '/' unless base_uri.end_with?('/') - @request = HTTPClient.new({ - :base_url => base_uri, - :default_header => { - 'Content-Type' => 'application/json' - } - }) - if auth && valid_json?(auth) - # Using Admin SDK service account - @credentials = Google::Auth::DefaultCredentials.make_creds( - json_key_io: StringIO.new(auth), - scope: %w(https://www.googleapis.com/auth/firebase.database https://www.googleapis.com/auth/userinfo.email) - ) - @credentials.apply!(@request.default_header) - @expires_at = @credentials.issued_at + 0.95 * @credentials.expires_in - else - # Using deprecated Database Secret - @secret = auth - end - end - - # Writes and returns the data - # Firebase.set('users/info', { 'name' => 'Oscar' }) => { 'name' => 'Oscar' } - def set(path, data, query={}) - process :put, path, data, query - end - - # Returns the data at path - def get(path, query={}) - process :get, path, nil, query - end - - # Writes the data, returns the key name of the data added - # Firebase.push('users', { 'age' => 18}) => {"name":"-INOQPH-aV_psbk3ZXEX"} - def push(path, data, query={}) - process :post, path, data, query - end - - # Deletes the data at path and returs true - def delete(path, query={}) - process :delete, path, nil, query - end - - # Write the data at path but does not delete ommited children. Returns the data - # Firebase.update('users/info', { 'name' => 'Oscar' }) => { 'name' => 'Oscar' } - def update(path, data, query={}) - process :patch, path, data, query - end - - private - - def process(verb, path, data=nil, query={}) - if path[0] == '/' - raise(ArgumentError.new("Invalid path: #{path}. Path must be relative")) - end - - if @expires_at && Time.now > @expires_at - @credentials.refresh! - @credentials.apply! @request.default_header - @expires_at = @credentials.issued_at + 0.95 * @credentials.expires_in - end - - Firebase::Response.new @request.request(verb, "#{path}.json", { - :body => (data && data.to_json), - :query => (@secret ? { :auth => @secret }.merge(query) : query), - :follow_redirect => true - }) - end +require 'firebase/client' - def valid_json?(json) - begin - JSON.parse(json) - return true - rescue JSON::ParserError - return false - end - end - end -end +# Suggestion to slowly replace Client with Database +# So that we can have Firebase::Storeage, Firebase::RealtineDatabase +# in the future +Firebase::Database = Firebase::Client diff --git a/lib/firebase/client.rb b/lib/firebase/client.rb new file mode 100644 index 0000000..4bc0d0a --- /dev/null +++ b/lib/firebase/client.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Firebase + # All Database interactions implemented here + class Client + attr_reader :request + + def initialize(base_uri, auth = nil) + @request = Request.new(base_uri, auth) + end + + # Writes and returns the data + # Firebase.set('users/info', { 'name' => 'Oscar' }) => { 'name' => 'Oscar' } + def set(path, data, query = {}) + request.execute(:put, path, data, query) + end + + # Returns the data at path + def get(path, query = {}) + request.execute(:get, path, nil, query) + end + + # Writes the data, returns the key name of the data added + # Firebase.push('users', { 'age' => 18}) => {"name":"-INOQPH-aV_psbk3ZXEX"} + def push(path, data, query = {}) + request.execute(:post, path, data, query) + end + + # Deletes the data at path and returs true + def delete(path, query = {}) + request.execute(:delete, path, nil, query) + end + + # Write the data at path but does not delete ommited + # children. Returns the data + # Firebase.update('users/info', + # { 'name' => 'Oscar' }) => { 'name' => 'Oscar' } + def update(path, data, query = {}) + request.execute(:patch, path, data, query) + end + + # Aliasing methods to match usual Ruby/Rails http methods + alias post push + alias put set + alias destroy delete + end +end diff --git a/lib/firebase/request.rb b/lib/firebase/request.rb new file mode 100644 index 0000000..fc22425 --- /dev/null +++ b/lib/firebase/request.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'uri' +require 'httpclient' +require 'googleauth' +require 'json' + +module Firebase + # API requests to firebase API implemented here + class Request + attr_reader :http_client, :auth + + def initialize(uri, auth, headers = { 'Content-Type' => 'application/json' }) + raise ArgumentError.new('base_uri must be a valid https uri') if uri !~ URI.regexp(%w(https)) + uri += '/' unless uri.end_with?('/') + + @http_client = HTTPClient.new(base_url: uri, + default_header: headers) + + if valid_auth?(auth) + # Using Admin SDK private key + @credentials = Google::Auth::DefaultCredentials.make_creds( + json_key_io: StringIO.new(auth), + scope: %w(https://www.googleapis.com/auth/firebase.database https://www.googleapis.com/auth/userinfo.email) + ) + + @credentials.apply!(@http_client.default_header) + @expires_at = @credentials.issued_at + 0.95 * @credentials.expires_in + else + # Using deprecated Database Secret + @auth = auth + end + end + + def execute(method, path, data = nil, query = {}) + raise ArgumentError.new("Invalid path: #{path}. Path must be relative") if path.start_with? '/' + + if @expires_at && Time.now > @expires_at + @credentials.refresh! + @credentials.apply! http_client.default_header + @expires_at = @credentials.issued_at + 0.95 * @credentials.expires_in + end + + params = { body: (data && data.to_json), + query: (@auth ? { auth: @auth }.merge(query) : query), + follow_redirect: true } + + http_call = http_client.request(method, "#{path}.json", params) + Firebase::Response.new(http_call) + end + + private + + def valid_auth?(auth) + return false unless auth + + JSON.parse(auth) + return true + rescue TypeError + return false + rescue JSON::ParserError + return false + end + end +end diff --git a/lib/firebase/response.rb b/lib/firebase/response.rb index 55e5f3c..72174bb 100644 --- a/lib/firebase/response.rb +++ b/lib/firebase/response.rb @@ -1,4 +1,7 @@ +# frozen_string_literal: true + module Firebase + # Firebase response class class Response attr_accessor :response @@ -7,7 +10,7 @@ def initialize(response) end def body - JSON.parse(response.body, :quirks_mode => true) + JSON.parse(response.body, quirks_mode: true) end def raw_body diff --git a/lib/firebase/server_value.rb b/lib/firebase/server_value.rb index 02604ed..68522bc 100644 --- a/lib/firebase/server_value.rb +++ b/lib/firebase/server_value.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module Firebase class ServerValue - TIMESTAMP = { ".sv" => 'timestamp' } + TIMESTAMP = { ".sv" => 'timestamp' }.freeze end end diff --git a/lib/firebase/version.rb b/lib/firebase/version.rb index 0a7fe04..5d194d1 100644 --- a/lib/firebase/version.rb +++ b/lib/firebase/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Firebase - VERSION = '0.2.8'.freeze + VERSION = '0.3.8'.freeze end diff --git a/spec/firebase_spec.rb b/spec/firebase_spec.rb index ab2652c..9a1c30b 100644 --- a/spec/firebase_spec.rb +++ b/spec/firebase_spec.rb @@ -24,16 +24,48 @@ @firebase = Firebase::Client.new('https://test.firebaseio.com') end - describe "set" do - it "writes and returns the data" do - expect(@firebase).to receive(:process).with(:put, 'users/info', data, {}) + describe 'initialize request' do + it 'returns firebase request object' do + expect(@firebase.request).to be_a(Firebase::Request) + end + + it 'returns nil auth' do + expect(@firebase.request.auth).to be nil + end + + it 'returns string auth token' do + auth_firebase = Firebase::Client.new('https://test.firebaseio.com', 'fakefirebasetoken') + expect(auth_firebase.request.auth).to eq 'fakefirebasetoken' + end + + it 'returns http_client object' do + expect(@firebase.request.http_client).to be_a HTTPClient + end + + it 'returns default header' do + expect(@firebase.request.http_client.default_header).to eq('Content-Type' => 'application/json') + end + + it 'returns base_url' do + expect(@firebase.request.http_client.base_url).to eq 'https://test.firebaseio.com/' + end + end + + describe 'set' do + it 'writes and returns the data' do + expect(@firebase.request).to receive(:execute).with(:put, 'users/info', data, {}) @firebase.set('users/info', data) end + + it 'writes and returns the data' do + expect(@firebase.request).to receive(:execute).with(:put, 'users/info', data, {}) + @firebase.put('users/info', data) + end end describe "get" do it "returns the data" do - expect(@firebase).to receive(:process).with(:get, 'users/info', nil, {}) + expect(@firebase.request).to receive(:execute).with(:get, 'users/info', nil, {}) @firebase.get('users/info') end @@ -42,7 +74,7 @@ :orderBy => '"$key"', :startAt => '"A1"' } - expect(@firebase).to receive(:process).with(:get, 'users/info', nil, params) + expect(@firebase.request).to receive(:execute).with(:get, 'users/info', nil, params) @firebase.get('users/info', params) end @@ -102,23 +134,33 @@ end end - describe "push" do - it "writes the data" do - expect(@firebase).to receive(:process).with(:post, 'users', data, {}) + describe 'push/post' do + it 'writes the data' do + expect(@firebase.request).to receive(:execute).with(:post, 'users', data, {}) @firebase.push('users', data) end + + it 'writes the data' do + expect(@firebase.request).to receive(:execute).with(:post, 'users', data, {}) + @firebase.post('users', data) + end end - describe "delete" do - it "returns true" do - expect(@firebase).to receive(:process).with(:delete, 'users/info', nil, {}) + describe 'delete/destroy' do + it 'returns true' do + expect(@firebase.request).to receive(:execute).with(:delete, 'users/info', nil, {}) @firebase.delete('users/info') end + + it 'returns true' do + expect(@firebase.request).to receive(:execute).with(:delete, 'users/info', nil, {}) + @firebase.destroy('users/info') + end end describe "update" do it "updates and returns the data" do - expect(@firebase).to receive(:process).with(:patch, 'users/info', data, {}) + expect(@firebase.request).to receive(:execute).with(:patch, 'users/info', data, {}) @firebase.update('users/info', data) end end @@ -126,11 +168,12 @@ describe "http processing" do it "sends custom auth query" do firebase = Firebase::Client.new('https://test.firebaseio.com', 'secret') - expect(firebase.request).to receive(:request).with(:get, "todos.json", { + expect(firebase.request.http_client).to receive(:request).with(:get, 'todos.json', { :body => nil, :query => {:auth => "secret", :foo => 'bar'}, :follow_redirect => true }) + firebase.get('todos', :foo => 'bar') end end @@ -154,10 +197,8 @@ it "sets custom auth header" do client = Firebase::Client.new('https://test.firebaseio.com/', '{ "private_key": true }') - expect(client.request.default_header).to eql({ - 'Content-Type' => 'application/json', - :authorization => 'Bearer 1' - }) + + expect(client.request.http_client.default_header).to eql('Content-Type' => 'application/json', authorization: 'Bearer 1') end it "handles token expiry" do @@ -165,11 +206,12 @@ client = Firebase::Client.new('https://test.firebaseio.com/', '{ "private_key": true }') allow(Time).to receive(:now) { current_time + 3600 } expect(@credentials).to receive(:refresh!) + client.get 'dummy' - expect(client.request.default_header).to eql({ + + expect(client.request.http_client.default_header).to eql( 'Content-Type' => 'application/json', - :authorization => 'Bearer 2' - }) + authorization: 'Bearer 2') end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index dfe48e7..ef7ffb4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,11 +2,10 @@ $LOAD_PATH.unshift(File.dirname(__FILE__)) require 'rspec' require 'firebase' +require 'pry' # Requires supporting files with custom matchers and macros, etc, # in ./support/ and its subdirectories. Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} -RSpec.configure do |config| - -end +RSpec.configure do |config|; end