syncer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +10 -0
- data/Guardfile +15 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +7 -0
- data/lib/syncer/syncable_record.rb +161 -0
- data/lib/syncer/syncer.rb +21 -0
- data/lib/syncer/version.rb +3 -0
- data/lib/syncer.rb +4 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/account.rb +5 -0
- data/spec/support/active_record.rb +53 -0
- data/spec/support/factories.rb +20 -0
- data/spec/support/stock.rb +9 -0
- data/spec/support/trade.rb +6 -0
- data/spec/syncable_record_spec.rb +447 -0
- data/spec/syncer_spec.rb +142 -0
- data/syncer.gemspec +23 -0
- metadata +154 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard :bundler do
|
5
|
+
watch('Gemfile')
|
6
|
+
watch(/^.+\.gemspec/)
|
7
|
+
end
|
8
|
+
|
9
|
+
guard :rspec, :version => 2 do
|
10
|
+
watch(%r{^spec/.+_spec\.rb$})
|
11
|
+
watch(%r{^lib/syncer/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
12
|
+
watch('spec/spec_helper.rb') { "spec" }
|
13
|
+
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
|
14
|
+
end
|
15
|
+
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Harry Hornreich
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Syncer
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'syncer'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install syncer
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Kernel
|
4
|
+
# helper that returns object in Hash/Array structure similar to json
|
5
|
+
def to_rjson
|
6
|
+
JSON.parse(self.to_json)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class SyncableRecord < ActiveRecord::Base
|
11
|
+
# this is an abstract model that should be inherited from
|
12
|
+
self.abstract_class = true
|
13
|
+
|
14
|
+
validates_presence_of :version
|
15
|
+
validates_numericality_of :version, :greater_than_or_equal_to => 0
|
16
|
+
|
17
|
+
before_validation :zero_version, :on => :create
|
18
|
+
after_validation :increment_version, :on => :update
|
19
|
+
|
20
|
+
def zero_version
|
21
|
+
self.version = 0
|
22
|
+
end
|
23
|
+
|
24
|
+
def increment_version
|
25
|
+
increment!(:version)
|
26
|
+
end
|
27
|
+
|
28
|
+
# helper to match record with remote record info
|
29
|
+
# if remote record has version use it otherwise only id matches (for delete records)
|
30
|
+
def matches?(remote_record)
|
31
|
+
if remote_record['version']
|
32
|
+
id == remote_record['id'] && version >= remote_record['version']
|
33
|
+
else
|
34
|
+
id == remote_record['id']
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# helper that checks if remote record has been updated
|
39
|
+
def updated?(remote_record)
|
40
|
+
id == remote_record['id'] && version > remote_record['version']
|
41
|
+
end
|
42
|
+
|
43
|
+
# finds models associated with temp ids and assigns their real id to the record
|
44
|
+
def self.fix_associated_ids(remote_record)
|
45
|
+
remote_record.keys.each do |key|
|
46
|
+
# if the key is of the form xxx_tmp_id
|
47
|
+
next unless /(?<foreign_key_prefix>.*)_tmp_id$/ =~ key
|
48
|
+
# then find the xxx class
|
49
|
+
next unless model_class = foreign_key_prefix.capitalize.to_class
|
50
|
+
# find the class model that matches the tmp_id
|
51
|
+
next unless model = model_class.find_by_tmp_id(remote_record["#{foreign_key_prefix}_tmp_id"])
|
52
|
+
# save the actual model id
|
53
|
+
remote_record["#{foreign_key_prefix}_id"] = model.id
|
54
|
+
# remove the xxx_tmp_id key & value
|
55
|
+
remote_record.delete(key)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# syncs a remote data source with server data state
|
60
|
+
# remote state should be provided as a Hash
|
61
|
+
# returns a Hash response with state changes for the remote data site
|
62
|
+
def self.sync(remote_state)
|
63
|
+
# TODO we do not currently check for id's per user
|
64
|
+
# TODO check both when creating and when accessing
|
65
|
+
# TODO we should at some point delete the tmp_ids
|
66
|
+
|
67
|
+
remote_saved = remote_state['saved'] || []
|
68
|
+
remote_created = remote_state['created'] || []
|
69
|
+
remote_deleted = remote_state['deleted'] || []
|
70
|
+
remote_updated = remote_state['updated'] || []
|
71
|
+
|
72
|
+
# build list of all the server records known to remote
|
73
|
+
remote_existing_records = remote_saved + remote_updated + remote_deleted
|
74
|
+
|
75
|
+
# group server records by new, updated and deleted as relates to remote state
|
76
|
+
all_records = self.all
|
77
|
+
new_records = all_records.find_all do |record|
|
78
|
+
!remote_existing_records.detect { |remote_record| record.matches?(remote_record) }
|
79
|
+
end
|
80
|
+
update_records = all_records.find_all do |record|
|
81
|
+
remote_existing_records.detect { |remote_record| record.updated?(remote_record) }
|
82
|
+
end
|
83
|
+
delete_records = remote_existing_records.find_all do |remote_record|
|
84
|
+
!all_records.detect { |record| record.matches?(remote_record) }
|
85
|
+
end
|
86
|
+
|
87
|
+
# created
|
88
|
+
created = new_records
|
89
|
+
|
90
|
+
# handle special case where we have an updated record that was deleted remotely
|
91
|
+
# in that case we must recreate it
|
92
|
+
special_records = []
|
93
|
+
update_records.each do |record|
|
94
|
+
match = remote_deleted.detect { |remote_record| record.updated?(remote_record) }
|
95
|
+
if match
|
96
|
+
created << record
|
97
|
+
special_records << record
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# cleanup by removing these special case records
|
102
|
+
# no need to remove from remote_deleted as that we will confirm
|
103
|
+
special_records.each { |record| update_records.delete(record) }
|
104
|
+
|
105
|
+
# create all new remote records and add to created list
|
106
|
+
if remote_created
|
107
|
+
remote_created.each do |remote_record|
|
108
|
+
fix_associated_ids(remote_record)
|
109
|
+
rec = self.create(remote_record)
|
110
|
+
if rec.persisted?
|
111
|
+
response = rec.to_rjson
|
112
|
+
created << response
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# updated
|
118
|
+
updated = update_records
|
119
|
+
|
120
|
+
# update all updated remote records and add to updated list
|
121
|
+
if remote_updated
|
122
|
+
remote_updated.each do |remote_record|
|
123
|
+
fix_associated_ids(remote_record)
|
124
|
+
rec = self.find_by_id(remote_record['id'])
|
125
|
+
# update the record only if found and if it has not been updated on the server already
|
126
|
+
if rec && !rec.updated?(remote_record)
|
127
|
+
attrs = remote_record.dup
|
128
|
+
attrs.delete("id") # can't update id
|
129
|
+
attrs.delete("version") # can't update version
|
130
|
+
updated << rec if rec.update_attributes(attrs)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# deleted
|
136
|
+
deleted = delete_records.map{ |r| {:id => r['id']} if r['id'] }
|
137
|
+
deleted.delete(nil)
|
138
|
+
|
139
|
+
# destroy all deleted remote records and add to deleted list
|
140
|
+
if remote_deleted
|
141
|
+
remote_deleted.each do |remote_record|
|
142
|
+
rec = self.find_by_id(remote_record['id'])
|
143
|
+
# destroy the record only if found and if it has not been updated on the server already
|
144
|
+
if rec && !rec.updated?(remote_record)
|
145
|
+
rec.destroy
|
146
|
+
deleted << { :id => remote_record['id'] } if remote_record['id']
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
response = Hash.new
|
152
|
+
response[:create] = created unless created.empty?
|
153
|
+
response[:update] = updated unless updated.empty?
|
154
|
+
response[:delete] = deleted unless deleted.empty?
|
155
|
+
|
156
|
+
response
|
157
|
+
|
158
|
+
rescue => error
|
159
|
+
{ :error => "#{error}" }
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'xstring'
|
2
|
+
|
3
|
+
class Syncer
|
4
|
+
SYNC_ORDER_KEY = '_sync_order'
|
5
|
+
|
6
|
+
# request keys should be strings and not symbols
|
7
|
+
def self.sync(request)
|
8
|
+
request = request.to_rjson
|
9
|
+
response = {};
|
10
|
+
all_sync_request = request
|
11
|
+
sync_order = all_sync_request[SYNC_ORDER_KEY];
|
12
|
+
# TODO if nil raise an exception
|
13
|
+
sync_order.each do |model|
|
14
|
+
model_sync_data = all_sync_request[model]
|
15
|
+
next unless model_sync_data # it is ok not to have data for each model in the order
|
16
|
+
sync_response = model.to_class.sync(model_sync_data)
|
17
|
+
response[model] = sync_response
|
18
|
+
end
|
19
|
+
response.to_rjson
|
20
|
+
end
|
21
|
+
end
|
data/lib/syncer.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper.rb"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
10
|
+
config.run_all_when_everything_filtered = true
|
11
|
+
# config.filter_run :focus
|
12
|
+
end
|
13
|
+
|
14
|
+
# require the gem files
|
15
|
+
require_relative '../lib/syncer'
|
16
|
+
|
17
|
+
# Requires supporting ruby files with custom matchers and macros, etc,
|
18
|
+
# in spec/support/ and its subdirectories.
|
19
|
+
Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f}
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# setup activerecord
|
2
|
+
|
3
|
+
# took idea from here -> http://iain.nl/testing-activerecord-in-isolation
|
4
|
+
|
5
|
+
# establish connection to memory db
|
6
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
7
|
+
|
8
|
+
# config activerecord to not include type in JSON
|
9
|
+
ActiveRecord::Base.include_root_in_json = false
|
10
|
+
|
11
|
+
# create tables needed for specing
|
12
|
+
ActiveRecord::Migration.verbose = false
|
13
|
+
|
14
|
+
ActiveRecord::Migration.create_table "accounts", :force => true do |t|
|
15
|
+
t.string "tmp_id"
|
16
|
+
t.integer "version"
|
17
|
+
t.string "name"
|
18
|
+
t.datetime "created_at"
|
19
|
+
t.datetime "updated_at"
|
20
|
+
end
|
21
|
+
|
22
|
+
ActiveRecord::Migration.create_table "stocks", :force => true do |t|
|
23
|
+
t.string "tmp_id"
|
24
|
+
t.integer "version"
|
25
|
+
t.string "market"
|
26
|
+
t.string "ticker"
|
27
|
+
t.string "name"
|
28
|
+
t.integer "index"
|
29
|
+
t.integer "account_id"
|
30
|
+
t.datetime "created_at"
|
31
|
+
t.datetime "updated_at"
|
32
|
+
end
|
33
|
+
|
34
|
+
ActiveRecord::Migration.create_table "trades", :force => true do |t|
|
35
|
+
t.string "tmp_id"
|
36
|
+
t.integer "version"
|
37
|
+
t.boolean "buy"
|
38
|
+
t.integer "amount"
|
39
|
+
t.datetime "date"
|
40
|
+
t.integer "stock_id"
|
41
|
+
t.datetime "created_at"
|
42
|
+
t.datetime "updated_at"
|
43
|
+
end
|
44
|
+
|
45
|
+
#setup transactions
|
46
|
+
RSpec.configure do |config|
|
47
|
+
config.around do |example|
|
48
|
+
ActiveRecord::Base.transaction do
|
49
|
+
example.run
|
50
|
+
raise ActiveRecord::Rollback
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'factory_girl'
|
2
|
+
|
3
|
+
FactoryGirl.define do
|
4
|
+
factory :account do
|
5
|
+
name 'Leumi'
|
6
|
+
end
|
7
|
+
|
8
|
+
factory :stock do
|
9
|
+
market 'il'
|
10
|
+
ticker '629014'
|
11
|
+
name 'Teva'
|
12
|
+
end
|
13
|
+
|
14
|
+
factory :trade do
|
15
|
+
amount 100
|
16
|
+
buy true
|
17
|
+
date DateTime.now
|
18
|
+
association :stock
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,447 @@
|
|
1
|
+
require 'spec_helper.rb'
|
2
|
+
|
3
|
+
describe "SyncableRecord" do
|
4
|
+
|
5
|
+
describe "with no server records" do
|
6
|
+
|
7
|
+
describe "and no remote records" do
|
8
|
+
|
9
|
+
it "it should return empty response" do
|
10
|
+
response = Stock.sync({})
|
11
|
+
response.should be_empty
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "and created remote records" do
|
16
|
+
before(:each) do
|
17
|
+
@stock1 = { :market => 'il', :ticker => 'Teva', :name => 'Teva Pharma', :tmp_id => '12' }
|
18
|
+
@stock2 = { :market => 'us', :ticker => 'AAPL', :name => 'Apple', :tmp_id => '13' }
|
19
|
+
end
|
20
|
+
|
21
|
+
it "it should create the records and return create response for them" do
|
22
|
+
remote_state = { 'created' => [@stock1, @stock2] }
|
23
|
+
|
24
|
+
response = Stock.sync(HashWithIndifferentAccess.new(remote_state))
|
25
|
+
|
26
|
+
# check they were created on server
|
27
|
+
stocks = Stock.all
|
28
|
+
stocks.length.should == 2
|
29
|
+
Stock.find_by_name('Teva Pharma').should_not be_nil
|
30
|
+
Stock.find_by_name('Apple').should_not be_nil
|
31
|
+
|
32
|
+
response[:create].length.should == 2
|
33
|
+
response[:create][0]['name'].should == @stock1[:name]
|
34
|
+
response[:create][0]['version'].should == 0
|
35
|
+
response[:create][0]['id'].should_not be_nil
|
36
|
+
response[:create][0]['tmp_id'].should == @stock1[:tmp_id]
|
37
|
+
response[:create][1]['name'].should == @stock2[:name]
|
38
|
+
response[:create][1]['version'].should == 0
|
39
|
+
response[:create][1]['id'].should_not be_nil
|
40
|
+
response[:create][1]['tmp_id'].should == @stock2[:tmp_id]
|
41
|
+
response[:delete].should be_nil
|
42
|
+
response[:update].should be_nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "with server records" do
|
48
|
+
before(:each) do
|
49
|
+
@stock1 = FactoryGirl.create(:stock)
|
50
|
+
@stock2 = FactoryGirl.create(:stock, :market => 'us')
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "and no remote records" do
|
54
|
+
|
55
|
+
it "it should return create for server records" do
|
56
|
+
response = Stock.sync({})
|
57
|
+
response[:create].length.should == 2
|
58
|
+
response[:create][0].should == @stock1
|
59
|
+
response[:create][1].should == @stock2
|
60
|
+
response[:delete].should be_nil
|
61
|
+
response[:update].should be_nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "already saved on remote" do
|
66
|
+
|
67
|
+
it "it should return empty response" do
|
68
|
+
remote_state = { 'saved' => [@stock1, @stock2] }
|
69
|
+
response = Stock.sync(remote_state)
|
70
|
+
response.should be_empty
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "one of the remote records is deleted" do
|
75
|
+
|
76
|
+
it "it should delete the deleted record and keep the saved record" do
|
77
|
+
remote_state = { 'saved' => [@stock1], 'deleted' => [@stock2] }
|
78
|
+
response = Stock.sync(remote_state)
|
79
|
+
response[:delete].length.should == 1
|
80
|
+
response[:delete][0][:id].should == @stock2.id
|
81
|
+
response[:create].should be_nil
|
82
|
+
response[:update].should be_nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "with new records on server" do
|
88
|
+
before(:each) do
|
89
|
+
@stock1 = FactoryGirl.create(:stock)
|
90
|
+
@new_stock = FactoryGirl.create(:stock, :market => 'us', :ticker => 'AMZN', :name => 'Amazon')
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "and some saved records on remote" do
|
94
|
+
|
95
|
+
it "it should return create for new records" do
|
96
|
+
remote_state = { 'saved' => [@stock1] }
|
97
|
+
|
98
|
+
response = Stock.sync(remote_state)
|
99
|
+
response[:create].length.should == 1
|
100
|
+
response[:create][0].should == @new_stock
|
101
|
+
response[:delete].should be_nil
|
102
|
+
response[:update].should be_nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe "and saved & created remote records" do
|
107
|
+
before(:each) do
|
108
|
+
@stock3 = { :market => 'il', :ticker => 'Teva', :name => 'Teva Pharma', :tmp_id => '12' }
|
109
|
+
@stock4 = { :market => 'us', :ticker => 'AAPL', :name => 'Apple', :tmp_id => '13' }
|
110
|
+
end
|
111
|
+
|
112
|
+
it "it should create the new and created records and return create response for them" do
|
113
|
+
remote_state = { 'saved' => [@stock1], 'created' => [@stock3, @stock4] }
|
114
|
+
|
115
|
+
response = Stock.sync(HashWithIndifferentAccess.new(remote_state))
|
116
|
+
|
117
|
+
# check they were created on server
|
118
|
+
stocks = Stock.all
|
119
|
+
stocks.length.should == 4
|
120
|
+
Stock.find_by_name('Teva Pharma').should_not be_nil
|
121
|
+
Stock.find_by_name('Apple').should_not be_nil
|
122
|
+
Stock.find_by_name('Amazon').should_not be_nil
|
123
|
+
|
124
|
+
# check response
|
125
|
+
response[:create].length.should == 3
|
126
|
+
response[:create][0]['name'].should == @new_stock[:name]
|
127
|
+
response[:create][0]['version'].should == @new_stock[:version]
|
128
|
+
response[:create][1]['name'].should == @stock3[:name]
|
129
|
+
response[:create][1]['version'].should == 0
|
130
|
+
response[:create][1]['id'].should_not be_nil
|
131
|
+
response[:create][1]['tmp_id'].should == @stock3[:tmp_id]
|
132
|
+
response[:create][2]['name'].should == @stock4[:name]
|
133
|
+
response[:create][2]['version'].should == 0
|
134
|
+
response[:create][2]['id'].should_not be_nil
|
135
|
+
response[:create][2]['tmp_id'].should == @stock4[:tmp_id]
|
136
|
+
response[:delete].should be_nil
|
137
|
+
response[:update].should be_nil
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe "with server saved, new, deleted and updated records" do
|
143
|
+
before(:each) do
|
144
|
+
@stock1 = FactoryGirl.create(:stock)
|
145
|
+
@stock2 = FactoryGirl.create(:stock, :market => 'us', :ticker => 'IBM', :name => 'International Business Machines')
|
146
|
+
@new_stock = FactoryGirl.create(:stock, :market => 'us', :ticker => 'AMZN', :name => 'Amazon')
|
147
|
+
@updated_stock = FactoryGirl.create(:stock, :market => 'us', :ticker => 'APPL', :name => 'Apple')
|
148
|
+
@deleted_stock = FactoryGirl.create(:stock, :market => 'il', :ticker => 'Chil', :name => 'Israel Chemicals')
|
149
|
+
end
|
150
|
+
|
151
|
+
describe "and only saved remote records" do
|
152
|
+
|
153
|
+
it "it should create the new, update the updated and delete the deleted" do
|
154
|
+
remote_state = { 'saved' => [@stock1, @stock2, @updated_stock.to_rjson, @deleted_stock] }
|
155
|
+
|
156
|
+
# do server changes: update a stock, delete a stock
|
157
|
+
@updated_stock.update_attributes({:name => "Apple Computers"})
|
158
|
+
@deleted_stock.destroy
|
159
|
+
|
160
|
+
response = Stock.sync(HashWithIndifferentAccess.new(remote_state))
|
161
|
+
|
162
|
+
# check nothing was created on server
|
163
|
+
stocks = Stock.all
|
164
|
+
stocks.length.should == 4
|
165
|
+
|
166
|
+
# check response
|
167
|
+
response[:create].length.should == 1
|
168
|
+
response[:create][0].should == @new_stock
|
169
|
+
response[:update].length.should == 1
|
170
|
+
response[:update][0].should == @updated_stock
|
171
|
+
@updated_stock[:version].should == 1
|
172
|
+
@updated_stock[:name].should == "Apple Computers"
|
173
|
+
response[:delete].length.should == 1
|
174
|
+
response[:delete][0][:id].should == @deleted_stock.id
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
describe "and saved & deleted remote records" do
|
179
|
+
|
180
|
+
it "it should create the new, delete the deleted and update the updated" do
|
181
|
+
remote_state = { 'saved' => [@stock2, @updated_stock.to_rjson, @deleted_stock], 'deleted' => [@stock1] }
|
182
|
+
|
183
|
+
# do server changes: update a stock, delete a stock
|
184
|
+
@updated_stock.update_attributes({:name => "Apple Computers"})
|
185
|
+
@deleted_stock.destroy
|
186
|
+
|
187
|
+
response = Stock.sync(HashWithIndifferentAccess.new(remote_state))
|
188
|
+
|
189
|
+
# check record was deleted on server
|
190
|
+
stocks = Stock.all
|
191
|
+
stocks.length.should == 3
|
192
|
+
Stock.find_by_id(@stock1.id).should be_false
|
193
|
+
|
194
|
+
# check response
|
195
|
+
response[:create].length.should == 1
|
196
|
+
response[:create][0].should == @new_stock
|
197
|
+
response[:update].length.should == 1
|
198
|
+
response[:update][0].should == @updated_stock
|
199
|
+
response[:delete].length.should == 2
|
200
|
+
response[:delete][0][:id].should == @deleted_stock.id
|
201
|
+
response[:delete][1][:id].should == @stock1.id
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
describe "and saved & updated remote records" do
|
206
|
+
|
207
|
+
it "it should create the new, delete the deleted and update the updated" do
|
208
|
+
remote_update = @stock1
|
209
|
+
remote_update['name'] = 'This is an updated name'
|
210
|
+
remote_state = { 'saved' => [@stock1, @stock2, @updated_stock, @deleted_stock], 'updated' => [remote_update] }
|
211
|
+
remote_state = remote_state.to_rjson
|
212
|
+
|
213
|
+
# do server changes: update a stock, delete a stock
|
214
|
+
@updated_stock.update_attributes({:name => "Apple Computers"})
|
215
|
+
@deleted_stock.destroy
|
216
|
+
|
217
|
+
# update what we expect in the response
|
218
|
+
remote_update['version'] += 1
|
219
|
+
|
220
|
+
response = Stock.sync(HashWithIndifferentAccess.new(remote_state))
|
221
|
+
|
222
|
+
# check nothing was created on server and what needed to be updated was
|
223
|
+
stocks = Stock.all
|
224
|
+
stocks.length.should == 4
|
225
|
+
Stock.find_by_id(@stock1.id).name == remote_update['name']
|
226
|
+
|
227
|
+
# check response
|
228
|
+
response[:create].length.should == 1
|
229
|
+
response[:create][0].should == @new_stock
|
230
|
+
response[:update].length.should == 2
|
231
|
+
response[:update][0].should == @updated_stock
|
232
|
+
response[:update][1]['id'].should == remote_update['id']
|
233
|
+
response[:update][1]['name'].should == remote_update['name']
|
234
|
+
response[:update][1]['version'].should == remote_update['version']
|
235
|
+
response[:delete].length.should == 1
|
236
|
+
response[:delete][0][:id].should == @deleted_stock[:id]
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
describe "and saved, created, updated, and deleted remote records" do
|
241
|
+
|
242
|
+
it "it should create the new, delete the deleted and update the updated" do
|
243
|
+
remote_update = @stock1
|
244
|
+
remote_update['name'] = 'This is an updated name'
|
245
|
+
remote_create = { :market => 'il', :ticker => 'Razio', :name => 'Razio Oil', :tmp_id => '19' }
|
246
|
+
remote_state = { 'saved' => [@stock1.to_rjson, @updated_stock, @deleted_stock], 'created' => [remote_create], 'updated' => [remote_update], 'deleted' => [@stock2] }
|
247
|
+
remote_state = remote_state.to_rjson
|
248
|
+
|
249
|
+
# do server changes: update a stock, delete a stock
|
250
|
+
@updated_stock.update_attributes({:name => "Apple Computers"})
|
251
|
+
@deleted_stock.destroy
|
252
|
+
|
253
|
+
# update what we expect in the response
|
254
|
+
remote_update['version'] += 1
|
255
|
+
|
256
|
+
response = Stock.sync(HashWithIndifferentAccess.new(remote_state))
|
257
|
+
|
258
|
+
# check nothing was created on server and what needed to be updated was
|
259
|
+
stocks = Stock.all
|
260
|
+
stocks.length.should == 4
|
261
|
+
Stock.find_by_name(remote_create[:name]).name == remote_create['name']
|
262
|
+
Stock.find_by_id(@stock1.id).name == remote_update['name']
|
263
|
+
Stock.find_by_id(@stock2.id).should be_false
|
264
|
+
|
265
|
+
# check response
|
266
|
+
response[:create].length.should == 2
|
267
|
+
response[:create][0].should == @new_stock
|
268
|
+
response[:create][1]['name'].should == remote_create[:name]
|
269
|
+
response[:create][1]['version'].should == 0
|
270
|
+
response[:create][1]['id'].should_not be_nil
|
271
|
+
response[:create][1]['tmp_id'].should == remote_create[:tmp_id]
|
272
|
+
response[:update].length.should == 2
|
273
|
+
response[:update][0].should == @updated_stock
|
274
|
+
response[:update][1]['id'].should == remote_update['id']
|
275
|
+
response[:update][1]['name'].should == remote_update['name']
|
276
|
+
response[:update][1]['version'].should == remote_update['version']
|
277
|
+
response[:delete].length.should == 2
|
278
|
+
response[:delete][0][:id].should == @deleted_stock.id
|
279
|
+
response[:delete][1][:id].should == @stock2.id
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
describe "with conflicts" do
|
285
|
+
before(:each) do
|
286
|
+
@stock = FactoryGirl.create(:stock, :market => 'us', :ticker => 'IBM', :name => 'International Business Machines')
|
287
|
+
end
|
288
|
+
|
289
|
+
describe "between server deleted record and remote deleted record" do
|
290
|
+
|
291
|
+
it "it should delete the record" do
|
292
|
+
remote_state = { 'deleted' => [@stock.to_rjson] }
|
293
|
+
|
294
|
+
# do server changes: delete the stock
|
295
|
+
@stock.destroy
|
296
|
+
|
297
|
+
response = Stock.sync(remote_state)
|
298
|
+
|
299
|
+
# check nothing was created on server
|
300
|
+
stocks = Stock.all
|
301
|
+
stocks.length.should == 0
|
302
|
+
|
303
|
+
# check response
|
304
|
+
response[:create].should be_nil
|
305
|
+
response[:update].should be_nil
|
306
|
+
response[:delete].length.should == 1
|
307
|
+
response[:delete][0][:id].should == @stock.id
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
describe "between server deleted record and remote updated record" do
|
312
|
+
|
313
|
+
it "it should delete the record" do
|
314
|
+
remote_update = @stock.to_rjson
|
315
|
+
remote_update['name'] = 'Tabulation company'
|
316
|
+
|
317
|
+
remote_state = { 'updated' => [@stock.to_rjson] }
|
318
|
+
|
319
|
+
# do server changes: delete the stock
|
320
|
+
@stock.destroy
|
321
|
+
|
322
|
+
response = Stock.sync(remote_state)
|
323
|
+
|
324
|
+
# check nothing was created on server
|
325
|
+
stocks = Stock.all
|
326
|
+
stocks.length.should == 0
|
327
|
+
|
328
|
+
# check response
|
329
|
+
response[:create].should be_nil
|
330
|
+
response[:update].should be_nil
|
331
|
+
response[:delete].length.should == 1
|
332
|
+
response[:delete][0][:id].should == @stock.id
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
describe "between server updated record and remote deleted record" do
|
337
|
+
|
338
|
+
it "it should create the record as if new" do
|
339
|
+
remote_state = { 'deleted' => [@stock.to_rjson] }
|
340
|
+
|
341
|
+
# do server changes: update the stock
|
342
|
+
@stock.update_attributes({:name => 'Tabulation company'})
|
343
|
+
@stock = Stock.find(@stock.id)
|
344
|
+
|
345
|
+
response = Stock.sync(remote_state)
|
346
|
+
|
347
|
+
# check nothing has changed on server
|
348
|
+
stocks = Stock.all
|
349
|
+
stocks.length.should == 1
|
350
|
+
|
351
|
+
# check response
|
352
|
+
response[:create].length.should == 1
|
353
|
+
response[:create][0]['id'].should == @stock[:id]
|
354
|
+
response[:create][0]['name'].should == @stock[:name]
|
355
|
+
response[:update].should be_nil
|
356
|
+
response[:delete].should be_nil
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
describe "between server updated record and remote updated record" do
|
361
|
+
|
362
|
+
it "it should update the record with the server version" do
|
363
|
+
remote_update = @stock.to_rjson
|
364
|
+
remote_update['name'] = 'Tabulation company'
|
365
|
+
|
366
|
+
remote_state = { 'updated' => [remote_update] }
|
367
|
+
|
368
|
+
# do server changes: update the stock
|
369
|
+
@stock.update_attributes({:name => 'ICR'})
|
370
|
+
@stock = Stock.find(@stock.id)
|
371
|
+
|
372
|
+
response = Stock.sync(remote_state)
|
373
|
+
|
374
|
+
# check nothing has changed on server
|
375
|
+
stocks = Stock.all
|
376
|
+
stocks.length.should == 1
|
377
|
+
@stock.name.should == 'ICR'
|
378
|
+
|
379
|
+
# check response
|
380
|
+
# response = JSON.parse(json_response)
|
381
|
+
response[:create].should be_nil
|
382
|
+
response[:update].length.should == 1
|
383
|
+
response[:update][0]['id'].should == @stock[:id]
|
384
|
+
response[:update][0]['name'].should == @stock[:name]
|
385
|
+
response[:delete].should be_nil
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
describe "with bad information" do
|
391
|
+
|
392
|
+
describe "send ilegal data" do
|
393
|
+
|
394
|
+
it "it should return error with 'Ilegal JSON'" do
|
395
|
+
response = Stock.sync(123)
|
396
|
+
|
397
|
+
response.should == { :error => "can't convert String into Integer" }
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
describe "send nonsense data" do
|
402
|
+
|
403
|
+
it "it should return empty" do
|
404
|
+
response = Stock.sync({ "foo" => 123 })
|
405
|
+
|
406
|
+
response.should == {}
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
describe "send remote update id that does not exist" do
|
411
|
+
|
412
|
+
it "it should ignore it" do
|
413
|
+
@stock = { :market => 'il', :ticker => 'Teva', :name => 'Teva Pharma' }
|
414
|
+
|
415
|
+
remote_state = { 'updated' => [@stock] }
|
416
|
+
|
417
|
+
response = Stock.sync(remote_state)
|
418
|
+
response.should be_empty
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
describe "send remote update with ilegal attributes" do
|
423
|
+
|
424
|
+
it "it should ignore it" do
|
425
|
+
@stock = { :marketing => 'yyy', :ticker => 'Teva', :name => 'Teva Pharma' }
|
426
|
+
|
427
|
+
remote_state = { 'updated' => [@stock] }
|
428
|
+
|
429
|
+
response = Stock.sync(remote_state)
|
430
|
+
response.should be_empty
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
describe "send remote create with ilegal attributes" do
|
435
|
+
|
436
|
+
it "it should return error" do
|
437
|
+
@stock = { :marketing => 'yyy', :ticker => 'Teva', :name => 'Teva Pharma' }
|
438
|
+
|
439
|
+
remote_state = { 'created' => [@stock] }
|
440
|
+
|
441
|
+
response = Stock.sync(remote_state)
|
442
|
+
response[:error].should_not be_empty
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
end
|
data/spec/syncer_spec.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'spec_helper.rb'
|
2
|
+
|
3
|
+
describe "Sync.sync" do
|
4
|
+
|
5
|
+
let(:stock) { FactoryGirl.create(:stock) }
|
6
|
+
let(:syncOrder) { [:Account, :Stock, :Trade] }
|
7
|
+
let(:syncOrderKey) { Syncer::SYNC_ORDER_KEY }
|
8
|
+
|
9
|
+
it "should sync a single Model" do
|
10
|
+
updated_stock = { :name => 'Teva Pharma', :id => stock.id, :version => stock.version }
|
11
|
+
stocks_sync_data = { :updated => [ updated_stock ] }
|
12
|
+
all_sync_data = { syncOrderKey => syncOrder, :Stock => stocks_sync_data }
|
13
|
+
|
14
|
+
result = Syncer.sync(all_sync_data)
|
15
|
+
|
16
|
+
stock_results = result['Stock']
|
17
|
+
stock_results.should_not be_nil
|
18
|
+
update_stock = stock_results['update'][0]
|
19
|
+
update_stock['id'].should == stock.id
|
20
|
+
update_stock['version'].should == stock.version + 1
|
21
|
+
update_stock['market'].should == stock.market
|
22
|
+
update_stock['ticker'].should == stock.ticker
|
23
|
+
update_stock['name'].should == updated_stock[:name]
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should sync multiple independent Models" do
|
27
|
+
updated_stock = { :name => 'Teva Pharma', :id => stock.id, :version => stock.version }
|
28
|
+
stocks_sync_data = { :updated => [ updated_stock ] }
|
29
|
+
created_account = FactoryGirl.attributes_for(:account)
|
30
|
+
created_account['tmp_id'] = "12345abcde"
|
31
|
+
accounts_sync_data = { :created => [ created_account ] }
|
32
|
+
all_sync_data = { syncOrderKey => syncOrder, :Stock => stocks_sync_data, :Account => accounts_sync_data }
|
33
|
+
|
34
|
+
result = Syncer.sync(all_sync_data)
|
35
|
+
|
36
|
+
stock_results = result['Stock']
|
37
|
+
stock_results.should_not be_nil
|
38
|
+
update_stock = stock_results['update'][0]
|
39
|
+
update_stock['id'].should == stock.id
|
40
|
+
update_stock['version'].should == stock.version + 1
|
41
|
+
update_stock['market'].should == stock.market
|
42
|
+
update_stock['ticker'].should == stock.ticker
|
43
|
+
update_stock['name'].should == updated_stock[:name]
|
44
|
+
|
45
|
+
account_results = result['Account']
|
46
|
+
account_results.should_not be_nil
|
47
|
+
create_account = account_results['create'][0]
|
48
|
+
create_account['id'].should_not be_nil
|
49
|
+
create_account['tmp_id'].should == "12345abcde"
|
50
|
+
create_account['version'].should == 0
|
51
|
+
create_account['name'].should == created_account[:name]
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should sync dependent Models with new model where dependency exists" do
|
55
|
+
updated_stock = { :name => 'Teva Pharma', :id => stock.id, :version => stock.version }
|
56
|
+
stocks_sync_data = { :updated => [ updated_stock ] }
|
57
|
+
created_trade = FactoryGirl.attributes_for(:trade, :stock_id => stock.id)
|
58
|
+
created_trade['tmp_id'] = "12345abcde"
|
59
|
+
trades_sync_data = { :created => [ created_trade ] }
|
60
|
+
all_sync_data = { syncOrderKey => syncOrder, :Trade => trades_sync_data, :Stock => stocks_sync_data }
|
61
|
+
|
62
|
+
result = Syncer.sync(all_sync_data)
|
63
|
+
|
64
|
+
stock_results = result['Stock']
|
65
|
+
update_stock = stock_results['update'][0]
|
66
|
+
update_stock['id'].should == stock.id
|
67
|
+
update_stock['version'].should == stock.version + 1
|
68
|
+
update_stock['market'].should == stock.market
|
69
|
+
update_stock['ticker'].should == stock.ticker
|
70
|
+
update_stock['name'].should == updated_stock[:name]
|
71
|
+
|
72
|
+
trade_results = result['Trade']
|
73
|
+
create_trade = trade_results['create'][0]
|
74
|
+
create_trade['id'].should_not be_nil
|
75
|
+
create_trade['tmp_id'].should == "12345abcde"
|
76
|
+
create_trade['version'].should == 0
|
77
|
+
create_trade['stock_id'].should == stock.id
|
78
|
+
create_trade['amount'].should == created_trade[:amount]
|
79
|
+
create_trade['buy'].should == created_trade[:buy]
|
80
|
+
# TODO maybe also test date that seems to return different ...
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should sync dependent Models when both models are new" do
|
84
|
+
created_stock = FactoryGirl.attributes_for(:stock, :name => 'Teva Pharma')
|
85
|
+
created_stock['tmp_id'] = "abcde12345"
|
86
|
+
stocks_sync_data = { :saved => [ stock ], :created => [ created_stock ] }
|
87
|
+
created_trade = FactoryGirl.attributes_for(:trade)
|
88
|
+
created_trade['tmp_id'] = "12345abcde"
|
89
|
+
created_trade['stock_tmp_id'] = "abcde12345"
|
90
|
+
trades_sync_data = { :created => [ created_trade ] }
|
91
|
+
all_sync_data = { syncOrderKey => syncOrder, :Trade => trades_sync_data, :Stock => stocks_sync_data }
|
92
|
+
|
93
|
+
result = Syncer.sync(all_sync_data)
|
94
|
+
|
95
|
+
stock_results = result['Stock']
|
96
|
+
create_stock = stock_results['create'][0]
|
97
|
+
create_stock['id'].should_not be_nil
|
98
|
+
create_stock['tmp_id'].should == "abcde12345"
|
99
|
+
create_stock['version'].should == 0
|
100
|
+
create_stock['market'].should == created_stock[:market]
|
101
|
+
create_stock['ticker'].should == created_stock[:ticker]
|
102
|
+
create_stock['name'].should == created_stock[:name]
|
103
|
+
|
104
|
+
trade_results = result['Trade']
|
105
|
+
create_trade = trade_results['create'][0]
|
106
|
+
create_trade['id'].should_not be_nil
|
107
|
+
create_trade['tmp_id'].should == "12345abcde"
|
108
|
+
create_trade['stock_id'].should == create_stock['id']
|
109
|
+
create_trade['version'].should == 0
|
110
|
+
create_trade['amount'].should == created_trade[:amount]
|
111
|
+
create_trade['buy'].should == created_trade[:buy]
|
112
|
+
# TODO maybe also test date that seems to return different ...
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should sync dependent Models with new model where dependent exists and is updated to point to new Model" do
|
116
|
+
updated_stock = { :name => 'Teva Pharma', :id => stock.id, :version => stock.version, :account_tmp_id => "12345abcde" }
|
117
|
+
stocks_sync_data = { :updated => [ updated_stock ] }
|
118
|
+
created_account = FactoryGirl.attributes_for(:account)
|
119
|
+
created_account['tmp_id'] = "12345abcde"
|
120
|
+
accounts_sync_data = { :created => [ created_account ] }
|
121
|
+
all_sync_data = { syncOrderKey => syncOrder, :Account => accounts_sync_data, :Stock => stocks_sync_data }
|
122
|
+
|
123
|
+
result = Syncer.sync(all_sync_data)
|
124
|
+
|
125
|
+
account_results = result['Account']
|
126
|
+
account_results.should_not be_nil
|
127
|
+
create_account = account_results['create'][0]
|
128
|
+
create_account['id'].should_not be_nil
|
129
|
+
create_account['tmp_id'].should == "12345abcde"
|
130
|
+
create_account['version'].should == 0
|
131
|
+
create_account['name'].should == created_account[:name]
|
132
|
+
|
133
|
+
stock_results = result['Stock']
|
134
|
+
update_stock = stock_results['update'][0]
|
135
|
+
update_stock['id'].should == stock.id
|
136
|
+
update_stock['version'].should == stock.version + 1
|
137
|
+
update_stock['market'].should == stock.market
|
138
|
+
update_stock['ticker'].should == stock.ticker
|
139
|
+
update_stock['name'].should == updated_stock[:name]
|
140
|
+
update_stock['account_id'].should == create_account['id']
|
141
|
+
end
|
142
|
+
end
|
data/syncer.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/syncer/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Harry Hornreich"]
|
6
|
+
gem.email = ["harryhorn@gmail.com"]
|
7
|
+
gem.description = %q{ActiveRecord syncer}
|
8
|
+
gem.summary = %q{ActiveRecord syncer}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.add_runtime_dependency 'activerecord', '>= 2.0'
|
12
|
+
gem.add_runtime_dependency 'xstring', '>= 0.0.7'
|
13
|
+
gem.add_development_dependency 'rspec', '>= 2.0'
|
14
|
+
gem.add_development_dependency 'sqlite3', '>= 1.3'
|
15
|
+
gem.add_development_dependency 'factory_girl', "~> 3.1.0"
|
16
|
+
|
17
|
+
gem.files = `git ls-files`.split($\)
|
18
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
19
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
20
|
+
gem.name = "syncer"
|
21
|
+
gem.require_paths = ["lib"]
|
22
|
+
gem.version = Syncer::VERSION
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: syncer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Harry Hornreich
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-12 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '2.0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: xstring
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 0.0.7
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.0.7
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: sqlite3
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '1.3'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '1.3'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: factory_girl
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 3.1.0
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 3.1.0
|
94
|
+
description: ActiveRecord syncer
|
95
|
+
email:
|
96
|
+
- harryhorn@gmail.com
|
97
|
+
executables: []
|
98
|
+
extensions: []
|
99
|
+
extra_rdoc_files: []
|
100
|
+
files:
|
101
|
+
- .gitignore
|
102
|
+
- .rspec
|
103
|
+
- Gemfile
|
104
|
+
- Guardfile
|
105
|
+
- LICENSE
|
106
|
+
- README.md
|
107
|
+
- Rakefile
|
108
|
+
- lib/syncer.rb
|
109
|
+
- lib/syncer/syncable_record.rb
|
110
|
+
- lib/syncer/syncer.rb
|
111
|
+
- lib/syncer/version.rb
|
112
|
+
- spec/spec_helper.rb
|
113
|
+
- spec/support/account.rb
|
114
|
+
- spec/support/active_record.rb
|
115
|
+
- spec/support/factories.rb
|
116
|
+
- spec/support/stock.rb
|
117
|
+
- spec/support/trade.rb
|
118
|
+
- spec/syncable_record_spec.rb
|
119
|
+
- spec/syncer_spec.rb
|
120
|
+
- syncer.gemspec
|
121
|
+
homepage: ''
|
122
|
+
licenses: []
|
123
|
+
post_install_message:
|
124
|
+
rdoc_options: []
|
125
|
+
require_paths:
|
126
|
+
- lib
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
128
|
+
none: false
|
129
|
+
requirements:
|
130
|
+
- - ! '>='
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
none: false
|
135
|
+
requirements:
|
136
|
+
- - ! '>='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
requirements: []
|
140
|
+
rubyforge_project:
|
141
|
+
rubygems_version: 1.8.21
|
142
|
+
signing_key:
|
143
|
+
specification_version: 3
|
144
|
+
summary: ActiveRecord syncer
|
145
|
+
test_files:
|
146
|
+
- spec/spec_helper.rb
|
147
|
+
- spec/support/account.rb
|
148
|
+
- spec/support/active_record.rb
|
149
|
+
- spec/support/factories.rb
|
150
|
+
- spec/support/stock.rb
|
151
|
+
- spec/support/trade.rb
|
152
|
+
- spec/syncable_record_spec.rb
|
153
|
+
- spec/syncer_spec.rb
|
154
|
+
has_rdoc:
|