zermelo 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/README.md +76 -52
  4. data/lib/zermelo/associations/association_data.rb +4 -3
  5. data/lib/zermelo/associations/class_methods.rb +37 -50
  6. data/lib/zermelo/associations/index.rb +3 -1
  7. data/lib/zermelo/associations/multiple.rb +247 -0
  8. data/lib/zermelo/associations/range_index.rb +44 -0
  9. data/lib/zermelo/associations/singular.rb +193 -0
  10. data/lib/zermelo/associations/unique_index.rb +4 -3
  11. data/lib/zermelo/backend.rb +120 -0
  12. data/lib/zermelo/backends/{influxdb_backend.rb → influxdb.rb} +87 -31
  13. data/lib/zermelo/backends/{redis_backend.rb → redis.rb} +53 -58
  14. data/lib/zermelo/backends/stub.rb +43 -0
  15. data/lib/zermelo/filter.rb +194 -0
  16. data/lib/zermelo/filters/index_range.rb +22 -0
  17. data/lib/zermelo/filters/{influxdb_filter.rb → influxdb.rb} +12 -11
  18. data/lib/zermelo/filters/redis.rb +173 -0
  19. data/lib/zermelo/filters/steps/list_step.rb +48 -30
  20. data/lib/zermelo/filters/steps/set_step.rb +148 -89
  21. data/lib/zermelo/filters/steps/sort_step.rb +2 -2
  22. data/lib/zermelo/record.rb +53 -0
  23. data/lib/zermelo/records/attributes.rb +32 -0
  24. data/lib/zermelo/records/class_methods.rb +12 -25
  25. data/lib/zermelo/records/{influxdb_record.rb → influxdb.rb} +3 -4
  26. data/lib/zermelo/records/instance_methods.rb +9 -8
  27. data/lib/zermelo/records/key.rb +3 -1
  28. data/lib/zermelo/records/redis.rb +17 -0
  29. data/lib/zermelo/records/stub.rb +17 -0
  30. data/lib/zermelo/version.rb +1 -1
  31. data/spec/lib/zermelo/associations/index_spec.rb +70 -1
  32. data/spec/lib/zermelo/associations/multiple_spec.rb +1084 -0
  33. data/spec/lib/zermelo/associations/range_index_spec.rb +77 -0
  34. data/spec/lib/zermelo/associations/singular_spec.rb +149 -0
  35. data/spec/lib/zermelo/associations/unique_index_spec.rb +58 -2
  36. data/spec/lib/zermelo/filter_spec.rb +363 -0
  37. data/spec/lib/zermelo/locks/redis_lock_spec.rb +3 -3
  38. data/spec/lib/zermelo/records/instance_methods_spec.rb +206 -0
  39. data/spec/spec_helper.rb +9 -1
  40. data/spec/support/mock_logger.rb +48 -0
  41. metadata +31 -46
  42. data/lib/zermelo/associations/belongs_to.rb +0 -115
  43. data/lib/zermelo/associations/has_and_belongs_to_many.rb +0 -128
  44. data/lib/zermelo/associations/has_many.rb +0 -120
  45. data/lib/zermelo/associations/has_one.rb +0 -109
  46. data/lib/zermelo/associations/has_sorted_set.rb +0 -124
  47. data/lib/zermelo/backends/base.rb +0 -115
  48. data/lib/zermelo/filters/base.rb +0 -212
  49. data/lib/zermelo/filters/redis_filter.rb +0 -111
  50. data/lib/zermelo/filters/steps/sorted_set_step.rb +0 -156
  51. data/lib/zermelo/records/base.rb +0 -62
  52. data/lib/zermelo/records/redis_record.rb +0 -27
  53. data/spec/lib/zermelo/associations/belongs_to_spec.rb +0 -6
  54. data/spec/lib/zermelo/associations/has_many_spec.rb +0 -6
  55. data/spec/lib/zermelo/associations/has_one_spec.rb +0 -6
  56. data/spec/lib/zermelo/associations/has_sorted_set.spec.rb +0 -6
  57. data/spec/lib/zermelo/backends/influxdb_backend_spec.rb +0 -0
  58. data/spec/lib/zermelo/backends/moneta_backend_spec.rb +0 -0
  59. data/spec/lib/zermelo/filters/influxdb_filter_spec.rb +0 -0
  60. data/spec/lib/zermelo/filters/redis_filter_spec.rb +0 -0
  61. data/spec/lib/zermelo/records/influxdb_record_spec.rb +0 -434
  62. data/spec/lib/zermelo/records/key_spec.rb +0 -6
  63. data/spec/lib/zermelo/records/redis_record_spec.rb +0 -1461
  64. data/spec/lib/zermelo/records/type_validator_spec.rb +0 -6
  65. data/spec/lib/zermelo/version_spec.rb +0 -6
  66. data/spec/lib/zermelo_spec.rb +0 -6
@@ -0,0 +1,206 @@
1
+ require 'spec_helper'
2
+ require 'zermelo/records/redis'
3
+ require 'zermelo/records/influxdb'
4
+
5
+ describe Zermelo::Records::InstMethods do
6
+
7
+ shared_examples "it supports ActiveModel instance methods", :instance_methods => true do
8
+
9
+ it "is invalid without a name" do
10
+ example = example_class.new(:id => '1')
11
+ expect(example).not_to be_valid
12
+
13
+ errs = example.errors
14
+ expect(errs).not_to be_nil
15
+ expect(errs[:name]).to eq(["can't be blank"])
16
+ end
17
+
18
+ it 'saves a record' do
19
+ example = example_class.new(:id => '1', :name => 'John Smith')
20
+ expect(example).to be_valid
21
+ expect(example_class.count).to eq(0)
22
+ expect(example.save).to be true
23
+ expect(example_class.count).to eq(1)
24
+ end
25
+
26
+ it "updates a value" do
27
+ create_example(:id => '1', :name => 'Jane Doe')
28
+
29
+ example = example_class.find_by_id('1')
30
+ expect(example).not_to be_nil
31
+
32
+ example.name = 'John Smith'
33
+ example.save
34
+
35
+ other_example = example_class.find_by_id('1')
36
+ expect(other_example).not_to be_nil
37
+ expect(other_example.name).to eq('John Smith')
38
+ end
39
+
40
+ it 'raises an RecordInvalid exception if validation fails while saving' do
41
+ example = example_class.new(:id => '1')
42
+
43
+ expect {
44
+ example.save!
45
+ }.to raise_error(Zermelo::Records::Errors::RecordInvalid)
46
+ end
47
+
48
+ it 'raises a RecordNotSaved exception if a callback blocks saving' do
49
+ example = example_class.new(:id => '1', :name => 'not_saving')
50
+
51
+ expect {
52
+ example.save!
53
+ }.to raise_error(Zermelo::Records::Errors::RecordNotSaved)
54
+ end
55
+
56
+ it "resets changed state on refresh" do
57
+ create_example(:id => '8', :name => 'John Jones')
58
+ example = example_class.find_by_id('8')
59
+
60
+ example.name = "King Henry VIII"
61
+ expect(example.changed).to include('name')
62
+ expect(example.changes).to eq({'name' => ['John Jones', 'King Henry VIII']})
63
+
64
+ example.refresh
65
+ expect(example.changed).to be_empty
66
+ expect(example.changes).to be_empty
67
+ end
68
+
69
+ it "stores a string as an attribute value"
70
+ it "stores an integer as an attribute value"
71
+ it "stores a timestamp as an attribute value"
72
+ it "stores a boolean as an attribute value"
73
+
74
+ it "stores a list as an attribute value"
75
+ it "stores a set as an attribute value"
76
+ it "stores a hash as an attribute value"
77
+
78
+ it 'destroys a record' do
79
+ create_example(:id => '1', :name => 'Jane Doe')
80
+
81
+ example = example_class.find_by_id('1')
82
+ example.destroy
83
+ example_chk = example_class.find_by_id('1')
84
+ expect(example_chk).to be_nil
85
+ end
86
+ end
87
+
88
+ context 'redis', :redis => true, :instance_methods => true do
89
+
90
+ module ZermeloExamples
91
+ class InstanceMethodsRedis
92
+ include Zermelo::Records::Redis
93
+
94
+ define_attributes :name => :string
95
+ validates :name, :presence => true
96
+
97
+ before_create :fail_if_not_saving
98
+ def fail_if_not_saving; !('not_saving'.eql?(self.name)); end
99
+ end
100
+ end
101
+
102
+ let(:redis) { Zermelo.redis }
103
+
104
+ let(:example_class) { ZermeloExamples::InstanceMethodsRedis }
105
+
106
+ let(:ek) { 'instance_methods_redis' }
107
+
108
+ def create_example(attrs = {})
109
+ redis.hmset("#{ek}:#{attrs[:id]}:attrs", {'name' => attrs[:name]}.to_a.flatten)
110
+ redis.sadd("#{ek}::attrs:ids", attrs[:id])
111
+ end
112
+
113
+ it 'creates data on record save' do
114
+ example = example_class.new(:id => '1', :name => 'John Smith')
115
+ expect(example).to be_valid
116
+ expect(example.save).to be true
117
+
118
+ expect(redis.keys('*')).to match_array([
119
+ "#{ek}::attrs:ids",
120
+ "#{ek}:1:attrs"
121
+ ])
122
+ expect(redis.smembers("#{ek}::attrs:ids")).to eq(['1'])
123
+ expect(redis.hgetall("#{ek}:1:attrs")).to eq(
124
+ 'name' => 'John Smith'
125
+ )
126
+ end
127
+
128
+ it "updates a record's attributes" do
129
+ create_example(:id => '8', :name => 'John Jones')
130
+
131
+ example = example_class.find_by_id('8')
132
+ example.name = 'Jane Janes'
133
+ expect(example.save).to be true
134
+
135
+ expect(redis.keys('*')).to match_array([
136
+ "#{ek}::attrs:ids",
137
+ "#{ek}:8:attrs"
138
+ ])
139
+ expect(redis.smembers("#{ek}::attrs:ids")).to eq(['8'])
140
+ expect(redis.hgetall("#{ek}:8:attrs")).to eq(
141
+ 'name' => 'Jane Janes'
142
+ )
143
+ end
144
+
145
+ it "deletes a record's attributes" do
146
+ create_example(:id => '8', :name => 'John Jones')
147
+
148
+ expect(redis.keys('*')).to match_array([
149
+ "#{ek}::attrs:ids",
150
+ "#{ek}:8:attrs",
151
+ ])
152
+
153
+ example = example_class.find_by_id('8')
154
+ example.destroy
155
+
156
+ expect(redis.keys('*')).to eq([])
157
+ end
158
+
159
+ end
160
+
161
+ context 'influxdb', :influxdb => true, :instance_methods => true do
162
+
163
+ module ZermeloExamples
164
+ class InstanceMethodsInfluxDB
165
+ include Zermelo::Records::InfluxDB
166
+
167
+ define_attributes :name => :string
168
+ validates :name, :presence => true
169
+
170
+ before_create :fail_if_not_saving
171
+ def fail_if_not_saving; !('not_saving'.eql?(self.name)); end
172
+ end
173
+ end
174
+
175
+ let(:influxdb) { Zermelo.influxdb }
176
+
177
+ let(:example_class) { ZermeloExamples::InstanceMethodsInfluxDB }
178
+
179
+ let(:ek) { 'instance_methods_influx_db' }
180
+
181
+ def create_example(attrs = {})
182
+ Zermelo.influxdb.write_point("#{ek}/#{attrs[:id]}", attrs)
183
+ end
184
+
185
+ it 'creates data on record save' do
186
+ begin
187
+ data = Zermelo.influxdb.query("select * from /#{ek}\\/1/")["#{ek}/1"]
188
+ expect(data).to be_nil
189
+ rescue InfluxDB::Error => ide
190
+ # only happens occasionally, with an empty time series by that name
191
+ raise unless /^Couldn't look up columns$/ === ide.message
192
+ end
193
+
194
+ example = example_class.new(:id => '1', :name => 'John Smith')
195
+ expect(example).to be_valid
196
+ expect(example.save).to be true
197
+
198
+ data = Zermelo.influxdb.query("select * from /#{ek}\\/1/")["#{ek}/1"]
199
+ expect(data).to be_an(Array)
200
+ expect(data.size).to eql(1)
201
+ record = data.first
202
+ expect(record).to be_a(Hash)
203
+ expect(record).to include("name"=>"John Smith", "id"=>"1")
204
+ end
205
+ end
206
+ end
data/spec/spec_helper.rb CHANGED
@@ -36,7 +36,15 @@ RSpec.configure do |config|
36
36
  # order dependency and want to debug it, you can fix the order by providing
37
37
  # the seed, which is printed after each run.
38
38
  # --seed 1234
39
- config.order = 'random'
39
+ # config.order = 'random'
40
+
41
+ config.around(:each, :logger => true) do |example|
42
+ MockLogger.configure_log('zermelo')
43
+ Zermelo.logger = MockLogger.new
44
+ example.run
45
+ puts Zermelo.logger.messages.compact.join("\n")
46
+ Zermelo.logger.clear
47
+ end
40
48
 
41
49
  config.before(:all, :redis => true) do
42
50
  Zermelo.redis = ::Redis.new(:db => 14)
@@ -0,0 +1,48 @@
1
+ class MockLogger
2
+ attr_accessor :messages, :errors
3
+
4
+ def self.configure_log(name, config = {})
5
+ @name = name
6
+ end
7
+
8
+ def initialize
9
+ @messages = []
10
+ @errors = []
11
+ end
12
+
13
+ def clear
14
+ @messages.clear
15
+ @errors.clear
16
+ end
17
+
18
+ %w(debug info warn).each do |level|
19
+ class_eval <<-RUBY
20
+ def #{level}(msg = nil, &block)
21
+ msg = yield if msg.nil? && block_given?
22
+ @messages << '[#{level.upcase}] :: ' +
23
+ (self.class.instance_variable_get('@name') || 'flapjack') + ' :: ' + msg
24
+ end
25
+ RUBY
26
+ end
27
+
28
+ %w(error fatal).each do |level|
29
+ class_eval <<-ERRORS
30
+ def #{level}(msg = nil, &block)
31
+ msg = yield if msg.nil? && block_given?
32
+ @messages << '[#{level.upcase}] :: ' +
33
+ (self.class.instance_variable_get('@name') || 'flapjack') + ' :: ' + msg
34
+ @errors << '[#{level.upcase}] :: ' +
35
+ (self.class.instance_variable_get('@name') || 'flapjack') + ' :: ' + msg
36
+ end
37
+ ERRORS
38
+ end
39
+
40
+ %w(debug info warn error fatal).each do |level|
41
+ class_eval <<-LEVELS
42
+ def #{level}?
43
+ true
44
+ end
45
+ LEVELS
46
+ end
47
+
48
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zermelo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ali Graham
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-09 00:00:00.000000000 Z
11
+ date: 2015-06-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -69,55 +69,48 @@ files:
69
69
  - Rakefile
70
70
  - lib/zermelo.rb
71
71
  - lib/zermelo/associations/association_data.rb
72
- - lib/zermelo/associations/belongs_to.rb
73
72
  - lib/zermelo/associations/class_methods.rb
74
- - lib/zermelo/associations/has_and_belongs_to_many.rb
75
- - lib/zermelo/associations/has_many.rb
76
- - lib/zermelo/associations/has_one.rb
77
- - lib/zermelo/associations/has_sorted_set.rb
78
73
  - lib/zermelo/associations/index.rb
79
74
  - lib/zermelo/associations/index_data.rb
75
+ - lib/zermelo/associations/multiple.rb
76
+ - lib/zermelo/associations/range_index.rb
77
+ - lib/zermelo/associations/singular.rb
80
78
  - lib/zermelo/associations/unique_index.rb
81
- - lib/zermelo/backends/base.rb
82
- - lib/zermelo/backends/influxdb_backend.rb
83
- - lib/zermelo/backends/redis_backend.rb
84
- - lib/zermelo/filters/base.rb
85
- - lib/zermelo/filters/influxdb_filter.rb
86
- - lib/zermelo/filters/redis_filter.rb
79
+ - lib/zermelo/backend.rb
80
+ - lib/zermelo/backends/influxdb.rb
81
+ - lib/zermelo/backends/redis.rb
82
+ - lib/zermelo/backends/stub.rb
83
+ - lib/zermelo/filter.rb
84
+ - lib/zermelo/filters/index_range.rb
85
+ - lib/zermelo/filters/influxdb.rb
86
+ - lib/zermelo/filters/redis.rb
87
87
  - lib/zermelo/filters/steps/base_step.rb
88
88
  - lib/zermelo/filters/steps/list_step.rb
89
89
  - lib/zermelo/filters/steps/set_step.rb
90
90
  - lib/zermelo/filters/steps/sort_step.rb
91
- - lib/zermelo/filters/steps/sorted_set_step.rb
92
91
  - lib/zermelo/locks/no_lock.rb
93
92
  - lib/zermelo/locks/redis_lock.rb
94
- - lib/zermelo/records/base.rb
93
+ - lib/zermelo/record.rb
94
+ - lib/zermelo/records/attributes.rb
95
95
  - lib/zermelo/records/class_methods.rb
96
96
  - lib/zermelo/records/errors.rb
97
- - lib/zermelo/records/influxdb_record.rb
97
+ - lib/zermelo/records/influxdb.rb
98
98
  - lib/zermelo/records/instance_methods.rb
99
99
  - lib/zermelo/records/key.rb
100
- - lib/zermelo/records/redis_record.rb
100
+ - lib/zermelo/records/redis.rb
101
+ - lib/zermelo/records/stub.rb
101
102
  - lib/zermelo/records/type_validator.rb
102
103
  - lib/zermelo/version.rb
103
- - spec/lib/zermelo/associations/belongs_to_spec.rb
104
- - spec/lib/zermelo/associations/has_many_spec.rb
105
- - spec/lib/zermelo/associations/has_one_spec.rb
106
- - spec/lib/zermelo/associations/has_sorted_set.spec.rb
107
104
  - spec/lib/zermelo/associations/index_spec.rb
105
+ - spec/lib/zermelo/associations/multiple_spec.rb
106
+ - spec/lib/zermelo/associations/range_index_spec.rb
107
+ - spec/lib/zermelo/associations/singular_spec.rb
108
108
  - spec/lib/zermelo/associations/unique_index_spec.rb
109
- - spec/lib/zermelo/backends/influxdb_backend_spec.rb
110
- - spec/lib/zermelo/backends/moneta_backend_spec.rb
111
- - spec/lib/zermelo/filters/influxdb_filter_spec.rb
112
- - spec/lib/zermelo/filters/redis_filter_spec.rb
109
+ - spec/lib/zermelo/filter_spec.rb
113
110
  - spec/lib/zermelo/locks/redis_lock_spec.rb
114
- - spec/lib/zermelo/records/influxdb_record_spec.rb
115
- - spec/lib/zermelo/records/key_spec.rb
116
- - spec/lib/zermelo/records/redis_record_spec.rb
117
- - spec/lib/zermelo/records/type_validator_spec.rb
118
- - spec/lib/zermelo/version_spec.rb
119
- - spec/lib/zermelo_spec.rb
111
+ - spec/lib/zermelo/records/instance_methods_spec.rb
120
112
  - spec/spec_helper.rb
113
+ - spec/support/mock_logger.rb
121
114
  - spec/support/profile_all_formatter.rb
122
115
  - spec/support/uncolored_doc_formatter.rb
123
116
  - zermelo.gemspec
@@ -142,28 +135,20 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
135
  requirements:
143
136
  - Redis and/or InfluxDB, and the related gems
144
137
  rubyforge_project:
145
- rubygems_version: 2.4.5
138
+ rubygems_version: 2.4.8
146
139
  signing_key:
147
140
  specification_version: 4
148
141
  summary: ActiveModel-based set-theoretic ORM for Redis/InfluxDB
149
142
  test_files:
150
- - spec/lib/zermelo/associations/belongs_to_spec.rb
151
- - spec/lib/zermelo/associations/has_many_spec.rb
152
- - spec/lib/zermelo/associations/has_one_spec.rb
153
- - spec/lib/zermelo/associations/has_sorted_set.spec.rb
154
143
  - spec/lib/zermelo/associations/index_spec.rb
144
+ - spec/lib/zermelo/associations/multiple_spec.rb
145
+ - spec/lib/zermelo/associations/range_index_spec.rb
146
+ - spec/lib/zermelo/associations/singular_spec.rb
155
147
  - spec/lib/zermelo/associations/unique_index_spec.rb
156
- - spec/lib/zermelo/backends/influxdb_backend_spec.rb
157
- - spec/lib/zermelo/backends/moneta_backend_spec.rb
158
- - spec/lib/zermelo/filters/influxdb_filter_spec.rb
159
- - spec/lib/zermelo/filters/redis_filter_spec.rb
148
+ - spec/lib/zermelo/filter_spec.rb
160
149
  - spec/lib/zermelo/locks/redis_lock_spec.rb
161
- - spec/lib/zermelo/records/influxdb_record_spec.rb
162
- - spec/lib/zermelo/records/key_spec.rb
163
- - spec/lib/zermelo/records/redis_record_spec.rb
164
- - spec/lib/zermelo/records/type_validator_spec.rb
165
- - spec/lib/zermelo/version_spec.rb
166
- - spec/lib/zermelo_spec.rb
150
+ - spec/lib/zermelo/records/instance_methods_spec.rb
167
151
  - spec/spec_helper.rb
152
+ - spec/support/mock_logger.rb
168
153
  - spec/support/profile_all_formatter.rb
169
154
  - spec/support/uncolored_doc_formatter.rb
@@ -1,115 +0,0 @@
1
- # The other side of a has_one, has_many, or has_sorted_set association
2
-
3
- module Zermelo
4
- module Associations
5
- class BelongsTo
6
-
7
- # NB a single instance of this class doesn't need to care about the hash
8
- # used for storage, that should be done in the save method of the parent
9
-
10
- def initialize(parent, name)
11
- @parent = parent
12
- @name = name
13
-
14
- @backend = parent.send(:backend)
15
-
16
- @record_ids_key = Zermelo::Records::Key.new(
17
- :klass => parent.class,
18
- :id => parent.id,
19
- :name => 'belongs_to',
20
- :type => :hash,
21
- :object => :association
22
- )
23
-
24
- parent.class.send(:with_association_data, name.to_sym) do |data|
25
- @associated_class = data.data_klass
26
- @lock_klasses = [data.data_klass] + data.related_klasses
27
- @inverse = data.inverse
28
- @callbacks = data.callbacks
29
- end
30
-
31
- raise ':inverse_of must be set' if @inverse.nil?
32
- @inverse_key = "#{name}_id"
33
- end
34
-
35
- def value=(record)
36
- if record.nil?
37
- @parent.class.lock(*@lock_klasses) do
38
- r = @associated_class.send(:load, @backend.get(@record_ids_key)[@inverse_key.to_s])
39
- bc = @callbacks[:before_clear]
40
- if bc.nil? || !@parent.respond_to?(bc) || !@parent.send(bc, r).is_a?(FalseClass)
41
- new_txn = @backend.begin_transaction
42
- @backend.delete(@record_ids_key, @inverse_key)
43
- @backend.commit_transaction if new_txn
44
- ac = @callbacks[:after_clear]
45
- @parent.send(ac, r) if !ac.nil? && @parent.respond_to?(ac)
46
- end
47
- end
48
- else
49
- raise 'Invalid record class' unless record.is_a?(@associated_class)
50
- raise 'Record must have been saved' unless record.persisted?
51
- @parent.class.lock(*@lock_klasses) do
52
- bs = @callbacks[:before_set]
53
- if bs.nil? || !@parent.respond_to?(bs) || !@parent.send(bs, r).is_a?(FalseClass)
54
- new_txn = @backend.begin_transaction
55
- @backend.add(@record_ids_key, @inverse_key => record.id)
56
- @backend.commit_transaction if new_txn
57
- as = @callbacks[:after_set]
58
- @parent.send(as, record) if !as.nil? && @parent.respond_to?(as)
59
- end
60
- end
61
- end
62
- end
63
-
64
- def value
65
- @parent.class.lock(*@lock_klasses) do
66
- # FIXME uses hgetall, need separate getter for hash/list/set
67
- if id = @backend.get(@record_ids_key)[@inverse_key.to_s]
68
- # if id = @backend.get_hash_value(@record_ids_key, @inverse_key.to_s)
69
- @associated_class.send(:load, id)
70
- else
71
- nil
72
- end
73
- end
74
- end
75
-
76
- private
77
-
78
- # on_remove already runs inside a lock & transaction
79
- def on_remove
80
- unless value.nil?
81
- assoc = value.send("#{@inverse}_proxy".to_sym)
82
- if assoc.respond_to?(:delete)
83
- assoc.send(:delete, @parent)
84
- elsif assoc.respond_to?(:value=)
85
- assoc.send(:value=, nil)
86
- end
87
- end
88
- @backend.clear(@record_ids_key)
89
- end
90
-
91
- def self.associated_ids_for(backend, klass, name, inversed, *these_ids)
92
- these_ids.each_with_object({}) do |this_id, memo|
93
- key = Zermelo::Records::Key.new(
94
- :klass => klass,
95
- :id => this_id,
96
- :name => 'belongs_to',
97
- :type => :hash,
98
- :object => :association
99
- )
100
-
101
- assoc_id = backend.get(key)["#{name}_id"]
102
- # assoc_id = backend.get_hash_value(key, "#{name}_id")
103
-
104
- if inversed
105
- memo[assoc_id] ||= []
106
- memo[assoc_id] << this_id
107
- else
108
- memo[this_id] = assoc_id
109
- end
110
- end
111
- end
112
-
113
- end
114
- end
115
- end
@@ -1,128 +0,0 @@
1
- require 'forwardable'
2
-
3
- # much like a has_many, but with different add/remove behaviour, as it's paired
4
- # with another has_and_belongs_to_many association. both sides must set the
5
- # inverse association name.
6
-
7
- module Zermelo
8
- module Associations
9
- class HasAndBelongsToMany
10
-
11
- extend Forwardable
12
-
13
- def_delegators :filter, :intersect, :union, :diff, :sort,
14
- :find_by_id, :find_by_ids, :find_by_id!, :find_by_ids!,
15
- :page, :all, :each, :collect, :map,
16
- :select, :find_all, :reject, :destroy_all,
17
- :ids, :count, :empty?, :exists?,
18
- :associated_ids_for
19
-
20
- def initialize(parent, name)
21
- @parent = parent
22
-
23
- @backend = parent.send(:backend)
24
-
25
- @record_ids_key = Zermelo::Records::Key.new(
26
- :klass => parent.class,
27
- :id => parent.id,
28
- :name => "#{name}_ids",
29
- :type => :set,
30
- :object => :association
31
- )
32
-
33
- parent.class.send(:with_association_data, name.to_sym) do |data|
34
- @associated_class = data.data_klass
35
- @lock_klasses = [data.data_klass] + data.related_klasses
36
- @inverse = data.inverse
37
- @callbacks = data.callbacks
38
- end
39
- end
40
-
41
- def <<(record)
42
- add(record)
43
- self # for << 'a' << 'b'
44
- end
45
-
46
- def add(*records)
47
- raise 'No records to add' if records.empty?
48
- raise 'Invalid record class' unless records.all? {|r| r.is_a?(@associated_class)}
49
- raise "Record(s) must have been saved" unless records.all? {|r| r.persisted?}
50
- @parent.class.lock(*@lock_klasses) do
51
- ba = @callbacks[:before_add]
52
- if ba.nil? || !@parent.respond_to?(ba) || !@parent.send(ba, *records).is_a?(FalseClass)
53
- records.each do |record|
54
- @associated_class.send(:load, record.id).send(@inverse.to_sym).
55
- send(:add_without_inverse, @parent)
56
- end
57
- add_without_inverse(*records)
58
- aa = @callbacks[:after_add]
59
- @parent.send(aa, *records) if !aa.nil? && @parent.respond_to?(aa)
60
- end
61
- end
62
- end
63
-
64
- # TODO support dependent delete, for now just deletes the association
65
- def delete(*records)
66
- raise 'No records to delete' if records.empty?
67
- raise 'Invalid record class' unless records.all? {|r| r.is_a?(@associated_class)}
68
- raise "Record(s) must have been saved" unless records.all? {|r| r.persisted?}
69
- @parent.class.lock(*@lock_klasses) do
70
- br = @callbacks[:before_remove]
71
- if br.nil? || !@parent.respond_to?(br) || !@parent.send(br, *records).is_a?(FalseClass)
72
- records.each do |record|
73
- @associated_class.send(:load, record.id).send(@inverse.to_sym).
74
- send(:delete_without_inverse, @parent)
75
- end
76
- delete_without_inverse(*records)
77
- ar = @callbacks[:after_remove]
78
- @parent.send(ar, *records) if !ar.nil? && @parent.respond_to?(ar)
79
- end
80
- end
81
- end
82
-
83
- private
84
-
85
- def add_without_inverse(*records)
86
- new_txn = @backend.begin_transaction
87
- @backend.add(@record_ids_key, records.map(&:id))
88
- @backend.commit_transaction if new_txn
89
- end
90
-
91
- def delete_without_inverse(*records)
92
- new_txn = @backend.begin_transaction
93
- @backend.delete(@record_ids_key, records.map(&:id))
94
- @backend.commit_transaction if new_txn
95
- end
96
-
97
- # associated will be the other side of the HaBTM; on_remove is always
98
- # called inside a lock
99
- def on_remove
100
- ids.each do |record_id|
101
- @associated_class.send(:load, record_id).send(@inverse.to_sym).
102
- send(:delete_without_inverse, @parent)
103
- end
104
- @backend.purge(@record_ids_key)
105
- end
106
-
107
- # creates a new filter class each time it's called, to store the
108
- # state for this particular filter chain
109
- def filter
110
- @backend.filter(@record_ids_key, @associated_class)
111
- end
112
-
113
- def self.associated_ids_for(backend, klass, name, *these_ids)
114
- these_ids.each_with_object({}) do |this_id, memo|
115
- key = Zermelo::Records::Key.new(
116
- :klass => klass,
117
- :id => this_id,
118
- :name => "#{name}_ids",
119
- :type => :set,
120
- :object => :association
121
- )
122
- memo[this_id] = backend.get(key)
123
- end
124
- end
125
-
126
- end
127
- end
128
- end