zermelo 1.1.0 → 1.2.0

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.
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