syncer 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|