Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for transaction #105

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 38 additions & 9 deletions lib/firebase.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,24 @@ def initialize(base_uri, auth=nil, scope=%w(https://www.googleapis.com/auth/fire

# Writes and returns the data
# Firebase.set('users/info', { 'name' => 'Oscar' }) => { 'name' => 'Oscar' }
def set(path, data, query={})
process :put, path, data, query
def set(path, data, query={}, header={})
process :put, path, data, query, header
end

# Returns the data at path
def get(path, query={})
process :get, path, nil, query
def get(path, query={}, header={})
process :get, path, nil, query, header
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
def push(path, data, query={}, header={})
process :post, path, data, query, header
end

# Deletes the data at path and returs true
def delete(path, query={})
process :delete, path, nil, query
def delete(path, query={}, header={})
process :delete, path, nil, query, header
end

# Write the data at path but does not delete ommited children. Returns the data
Expand All @@ -62,9 +62,37 @@ def update(path, data, query={})
process :patch, path, data, query
end

# Writes the data at path in a transactional manner. So the set fails if the value changes in the meantime. Returns the data
# Supply a block to return the new value of the node
# firebase_client.transaction("users/info", max_retries: 0) do |user_info_snapshot|
# user_info_snapshot["name"] = "new_name"
# user_info_snapshot
# end
def transaction(path, max_retries: nil, &block)
response = get path, {}, { "X-Firebase-ETag": true }
return response unless response.success?

etag_value = response.etag
data = response.body

data = block.call(data)

no_of_retries = 0
loop do
response = set path, data, {}, { "if-match": etag_value }
break if response.success?

no_of_retries += 1
etag_value = response.etag
break if max_retries && no_of_retries > max_retries
end

response
end

private

def process(verb, path, data=nil, query={})
def process(verb, path, data=nil, query={}, header= {})
if path[0] == '/'
raise(ArgumentError.new("Invalid path: #{path}. Path must be relative"))
end
Expand All @@ -78,6 +106,7 @@ def process(verb, path, data=nil, query={})
Firebase::Response.new @request.request(verb, "#{path}.json", {
:body => data.to_json,
:query => (@secret ? { :auth => @secret }.merge(query) : query),
:header => header,
:follow_redirect => true
})
end
Expand Down
4 changes: 4 additions & 0 deletions lib/firebase/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,9 @@ def success?
def code
response.status
end

def etag
response.headers['ETag']
end
end
end
78 changes: 73 additions & 5 deletions spec/firebase_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@

describe "set" do
it "writes and returns the data" do
expect(@firebase).to receive(:process).with(:put, 'users/info', data, {})
expect(@firebase).to receive(:process).with(:put, 'users/info', data, {}, {})
@firebase.set('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).to receive(:process).with(:get, 'users/info', nil, {}, {})
@firebase.get('users/info')
end

Expand All @@ -42,7 +42,7 @@
:orderBy => '"$key"',
:startAt => '"A1"'
}
expect(@firebase).to receive(:process).with(:get, 'users/info', nil, params)
expect(@firebase).to receive(:process).with(:get, 'users/info', nil, params, {})
@firebase.get('users/info', params)
end

Expand Down Expand Up @@ -73,14 +73,14 @@

describe "push" do
it "writes the data" do
expect(@firebase).to receive(:process).with(:post, 'users', data, {})
expect(@firebase).to receive(:process).with(:post, 'users', data, {}, {})
@firebase.push('users', data)
end
end

describe "delete" do
it "returns true" do
expect(@firebase).to receive(:process).with(:delete, 'users/info', nil, {})
expect(@firebase).to receive(:process).with(:delete, 'users/info', nil, {}, {})
@firebase.delete('users/info')
end
end
Expand All @@ -92,12 +92,80 @@
end
end

describe '#transaction' do
let(:etag_value) { 'i0Ir/zYOL6grKBc07+n2ncm/6as=' }
let(:new_data) { {'name' => 'Oscar Wilde'} }
let(:path) { 'users/info' }
let(:mock_etag_response) do
Firebase::Response.new(double({
:body => data.to_json,
:status => 200,
:headers => {
'ETag' => etag_value
}
}))
end
let(:mock_set_success_response) do
Firebase::Response.new(double({
:body => new_data.to_json,
:status => 200
}))
end

after(:each) do |example|
block_data = nil
resp = @firebase.transaction(path, max_retries: example.metadata[:max_retries]) do |data|
block_data = JSON.parse data.to_json
new_data
end
expect(block_data).to eql(data)
if example.metadata[:max_retries].nil?
expect(resp.body).to eql(new_data)
else
expect(resp.body).to eql(data)
expect(resp.success?).to eql(false)
end
end

context "data at path does not change" do
it 'updates and returns the data' do
expect(@firebase).to receive(:get).with(path, {}, { "X-Firebase-ETag": true }).and_return(mock_etag_response)
expect(@firebase).to receive(:set).with(path, new_data, {}, { "if-match": etag_value }).and_return(mock_set_success_response)
end
end

context "data at path changes" do
let(:new_etag_value) { 'i0Ir/zYOL6grKBc09+n2ncm/7as=' }

before(:each) do
mock_set_error_response = Firebase::Response.new(double({
:body => data.to_json,
:status => 412,
:headers => {
'ETag' => new_etag_value
}
}))
expect(@firebase).to receive(:get).with(path, {}, { "X-Firebase-ETag": true }).and_return(mock_etag_response)
expect(@firebase).to receive(:set).with(path, new_data, {}, { "if-match": etag_value }).and_return(mock_set_error_response)
end

it "retries incase the data at path changes in the meantime" do
expect(@firebase).to receive(:set).with(path, new_data, {}, { "if-match": new_etag_value }).and_return(mock_set_success_response)
end

it "does not retry after max_retries is reached", max_retries: 0 do
expect(@firebase).not_to receive(:set).with(path, new_data, {}, { "if-match": new_etag_value })
end
end
end

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", {
:body => nil,
:query => {:auth => "secret", :foo => 'bar'},
:header => {},
:follow_redirect => true
})
firebase.get('todos', :foo => 'bar')
Expand Down