passive_record 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 538bfa9bf872e0e23675e06d820897253504c6c3
4
- data.tar.gz: ece1e692df2cfd1a30d9240fc4fdfca11388f8a3
3
+ metadata.gz: 1d9a56d9aea21fdf0049781ce8d07cf85d1e996a
4
+ data.tar.gz: 9817a28de29ec3e02268234a41562925cf04adc5
5
5
  SHA512:
6
- metadata.gz: 066ce8866a53c2725e9c8e642be67e0ac2dd86fbaeab3b435c354bcf6e30255b90b1082515721ed7c132246aa30a89e3d555e8611db38033f722f9e66cb075c5
7
- data.tar.gz: 9ed65e8664ff3c288ad8dbabe17efae62b8be398ea8f1849d97ac8e72f515bcdc02bef4c4b4af10e06ddb6c6e4817bd129873e4ad743f600e33f1f784c860d85
6
+ metadata.gz: 611cd1f3b49bf74734443e19d4b5c1a95f23f598d986d9ea34f57ce5685d206440e3591417f076bbd3020979c5b36165a8518318e5ad0436bccb074bc13febee
7
+ data.tar.gz: e0a1d8f0b8a67b4eaa526336740e818ba7ceffcf09171b1ed7456402b1dcfda7761a38645c6c7480513abf53b4f0ffa8872596e0574c8823c5ca347a3fb879c7
data/README.md CHANGED
@@ -104,7 +104,7 @@ PassiveRecord may be right for you!
104
104
  - `User.find_by(conditions_hash)`
105
105
  - `User.find_all_by(conditions_hash)`
106
106
  - `User.all` and `User.each`
107
- - User is `Enumerable` through `User.all`, so we have `User.count`, `User.first`, etc.
107
+ - `User.each` enumerates over `User.all`, giving `User.count`, `User.first`, etc.
108
108
  - `User.where(conditions_hash)` (returns a `PassiveRecord::Query` object)
109
109
  - `User.descendants`
110
110
  - `User.destroy_all`
@@ -130,11 +130,11 @@ PassiveRecord may be right for you!
130
130
  - `parent.child_id=`
131
131
  - `parent.create_child(attrs)`
132
132
 
133
- ### Has Many
133
+ ### Has Many / Has Many Through / HABTM
134
134
 
135
- A model `Parent` which declares `has_many :children` will gain:
135
+ A model `Parent` which declares `has_many :children` or `has_and_belongs_to_many :children` will gain:
136
136
 
137
- - `parent.children` (returns a `Relation`)
137
+ - `parent.children` (returns a `Relation`, documented below)
138
138
  - `parent.children_ids`
139
139
  - `parent.children=`
140
140
  - `parent.children_ids=`
@@ -144,9 +144,31 @@ PassiveRecord may be right for you!
144
144
  - `parent.children.empty?`
145
145
  - `parent.children.where` (returns a `Core::Query`)
146
146
 
147
+ ### Explicit Relations
148
+
149
+ Parent models which declare `has_many :children` gain a `parent.children` instance that returns an explicit PassiveRecord relation object, which has the following public interface:
150
+
151
+ - `parent.children.all`
152
+ - `parent.children.each` enumerates over `parent.children.all`, giving `parent.children.count`, `parent.children.first`, etc.
153
+ - `parent.children.all?(&predicate)`
154
+ - `parent.children.empty?`
155
+ - `parent.children.where(conditions)` (returns a `Core::Query`)
156
+ - `parent.children<<` (insert a new child into the relation)
157
+
158
+ ### Queries
159
+
160
+ `Core::Query` objects acquired through `where` are chainable and have their own API.
161
+
162
+ - `where(conditions).all`
163
+ - `where(conditions).each` enumerates over `where(conditions).all`, so we have `where(conditions).count`, `where(conditions).first`, etc.
164
+ - `where(conditions).create(attrs)`
165
+ - `where(conditions).first_or_create(attrs)`
166
+ - `where(conditions).where(further_conditions)` (chaining)
167
+
147
168
  ## Hooks
148
169
 
149
- - `after_create :call_method`
170
+ - `before_create :call_a_method`
171
+ - `after_create :call_another_method, :and_then_call_another_one`
150
172
  - `after_update { or_use_a_block }`
151
173
 
152
174
  ## Copyright
@@ -15,7 +15,7 @@ module PassiveRecord
15
15
  @associations&.map do |assn|
16
16
  if assn.is_a?(HasOneAssociation) || assn.is_a?(BelongsToAssociation)
17
17
  (assn.target_name_symbol.to_s + "_id").to_sym
18
- else # plural ids
18
+ else
19
19
  (assn.target_name_symbol.to_s.singularize + "_ids").to_sym
20
20
  end
21
21
  end || []
@@ -50,5 +50,32 @@ module PassiveRecord
50
50
  associate!(association)
51
51
  end
52
52
  end
53
+
54
+ def has_and_belongs_to_many(collection_name_sym)
55
+ habtm_join_class_name =
56
+ self.name.split('::').last.singularize +
57
+ collection_name_sym.to_s.camelize.singularize +
58
+ "JoinModel"
59
+ inverse_habtm_join_class_name =
60
+ collection_name_sym.to_s.camelize.singularize +
61
+ self.name.split('::').last.singularize +
62
+ "JoinModel"
63
+
64
+ if (Object.const_get(inverse_habtm_join_class_name) rescue false)
65
+ has_many inverse_habtm_join_class_name.underscore.pluralize.to_sym
66
+ has_many collection_name_sym, :through => inverse_habtm_join_class_name.underscore.pluralize.to_sym
67
+ else
68
+ auto_collection_sym = self.name.split('::').last.underscore.pluralize.to_sym
69
+ eval <<-ruby
70
+ class ::#{habtm_join_class_name} # class UserRoleJoinModel
71
+ include PassiveRecord # include PassiveRecord
72
+ belongs_to :#{collection_name_sym.to_s.singularize} # belongs_to :role
73
+ belongs_to :#{auto_collection_sym.to_s.singularize} # belongs_to :user
74
+ end # end
75
+ ruby
76
+ has_many habtm_join_class_name.underscore.pluralize.to_sym
77
+ has_many(collection_name_sym, :through => habtm_join_class_name.underscore.pluralize.to_sym)
78
+ end
79
+ end
53
80
  end
54
81
  end
@@ -11,7 +11,6 @@ module PassiveRecord
11
11
  end
12
12
 
13
13
  class HasManyRelation < HasOneRelation
14
- include Enumerable
15
14
  include Enumerable
16
15
  extend Forwardable
17
16
 
@@ -28,13 +27,13 @@ module PassiveRecord
28
27
  all.empty?
29
28
  end
30
29
 
31
- def where(conditions)
32
- child_class.where(conditions.merge(parent_model_id_field => parent_model.id))
30
+ def where(conditions={})
31
+ child_class.where(conditions.merge(parent_model_id_field.to_sym => parent_model.id))
33
32
  end
34
-
33
+
35
34
  def <<(child)
36
35
  child.send(parent_model_id_field + "=", parent_model.id)
37
- lookup
36
+ all
38
37
  end
39
38
 
40
39
  def singular?
@@ -7,14 +7,11 @@ module PassiveRecord
7
7
  end
8
8
 
9
9
  class HasManyThroughRelation < HasManyRelation
10
- def intermediary_relation
11
- association.base_association.to_relation(parent_model)
12
- end
13
-
14
- def_delegators :all, :intermedia
15
-
16
- def results
17
- intermediary_relation.all
10
+ def <<(child)
11
+ intermediary_relation.
12
+ where((target_sym.to_s.singularize + "_id").to_sym => child.id).
13
+ first_or_create
14
+ all
18
15
  end
19
16
 
20
17
  def all
@@ -30,6 +27,15 @@ module PassiveRecord
30
27
  end
31
28
  end
32
29
 
30
+
31
+ def intermediary_relation
32
+ association.base_association.to_relation(parent_model)
33
+ end
34
+
35
+ def results
36
+ intermediary_relation.all
37
+ end
38
+
33
39
  def target_sym
34
40
  name_str = association.target_name_symbol.to_s
35
41
  singular_target_sym = name_str.singularize.to_sym
@@ -1,7 +1,6 @@
1
1
  module PassiveRecord
2
2
  module Associations
3
3
  class HasOneAssociation < Struct.new(:parent_class, :child_class_name, :child_name_sym)
4
-
5
4
  def to_relation(parent_model)
6
5
  HasOneRelation.new(self, parent_model)
7
6
  end
@@ -30,13 +30,10 @@ module PassiveRecord
30
30
  end
31
31
 
32
32
  def find_by(conditions)
33
- #if conditions.is_a?(Array)
34
- # find_by_ids(conditions)
35
33
  if conditions.is_a?(Hash)
36
34
  where(conditions).first
37
35
  else # assume we have an identifier/identifiers
38
36
  find(conditions)
39
- # find_by_id(conditions)
40
37
  end
41
38
  end
42
39
 
@@ -60,6 +57,10 @@ module PassiveRecord
60
57
 
61
58
  register(instance)
62
59
 
60
+ before_create_hooks.each do |hook|
61
+ hook.run(instance)
62
+ end
63
+
63
64
  attrs.each do |(k,v)|
64
65
  instance.send("#{k}=", v)
65
66
  end
@@ -1,45 +1,58 @@
1
1
  module PassiveRecord
2
2
  module Core
3
- class Query < Struct.new(:klass, :conditions)
3
+ class Query
4
+ include Enumerable
5
+ extend Forwardable
6
+
7
+ attr_accessor :klass, :conditions
8
+
9
+ def initialize(klass,conditions={})
10
+ @klass = klass
11
+ @conditions = conditions
12
+ end
13
+
4
14
  def all
5
15
  return [] unless conditions
6
-
7
- klass.all.select do |instance|
16
+ klass.select do |instance|
8
17
  conditions.all? do |(field,value)|
9
18
  if value.is_a?(Hash)
10
- assn = instance.send(field)
11
- matched = assn && value.all? do |(assn_field,val)|
12
- if assn.is_a?(Array)
13
- assn.any? do |assc_model|
14
- assc_model.send(assn_field) == val
15
- end
16
- else
17
- if assn.is_a?(Core::Query) || (assn.is_a?(Associations::Relation) && !assn.singular?)
18
- assn.where(assn_field => val) # send(assn_field) == val
19
- else
20
- assn.send(assn_field) == val
21
- end
22
- end
23
- end
24
- matched
19
+ evaluate_nested_conditions(instance, field, value)
25
20
  else
26
21
  instance.send(field) == value
27
22
  end
28
23
  end
29
24
  end
30
25
  end
26
+ def_delegators :all, :each
31
27
 
32
- def first
33
- all.first
34
- end
35
-
36
- def create
37
- klass.create(conditions)
28
+ def create(attrs={})
29
+ klass.create(conditions.merge(attrs))
38
30
  end
39
31
 
40
32
  def first_or_create
41
33
  first || create
42
34
  end
35
+
36
+ def where(new_conditions={})
37
+ @conditions = new_conditions.merge(conditions)
38
+ self
39
+ end
40
+
41
+ def ==(other_query)
42
+ @klass == other_query.klass && @conditions == other_query.conditions
43
+ end
44
+
45
+ protected
46
+ def evaluate_nested_conditions(instance, field, value)
47
+ association = instance.send(field)
48
+ association && value.all? do |(association_field,val)|
49
+ if association.is_a?(Associations::Relation) && !association.singular?
50
+ association.where(association_field => val).any?
51
+ else
52
+ association.send(association_field) == val
53
+ end
54
+ end
55
+ end
43
56
  end
44
57
  end
45
58
  end
@@ -20,6 +20,17 @@ module PassiveRecord
20
20
  end
21
21
  end
22
22
 
23
+ def before_create_hooks
24
+ @hooks ||= []
25
+ @hooks.select { |hook| hook.kind == :before_create }
26
+ end
27
+
28
+ def before_create(*meth_syms, &blk)
29
+ hook = Hook.new(:before_create,*meth_syms,&blk)
30
+ @hooks ||= []
31
+ @hooks += [ hook ]
32
+ end
33
+
23
34
  def after_create_hooks
24
35
  @hooks ||= []
25
36
  @hooks.select { |hook| hook.kind == :after_create }
@@ -59,7 +59,7 @@ module PassiveRecord
59
59
  end
60
60
 
61
61
  def find_relation_by_target_name_symbol(meth)
62
- relata.detect do |relation| # matching relation...
62
+ relata.detect do |relation|
63
63
  possible_target_names(relation).include?(meth.to_s)
64
64
  end
65
65
  end
@@ -90,12 +90,19 @@ module PassiveRecord
90
90
  end
91
91
  when "#{target_name}="
92
92
  if args.first.is_a?(Array)
93
- # need to loop through each arg and set id
94
- args.first.each do |child|
95
- child.send(matching_relation.parent_model_id_field + "=", id)
93
+ if matching_relation.is_a?(Associations::HasManyThroughRelation)
94
+ intermediary = matching_relation.intermediary_relation
95
+ args.first.each do |child|
96
+ intermediary.
97
+ where((target_name.singularize + "_id").to_sym => child.id).
98
+ first_or_create
99
+ end
100
+ else
101
+ args.first.each do |child|
102
+ child.send(matching_relation.parent_model_id_field + "=", id)
103
+ end
96
104
  end
97
105
  else
98
- # assume simple assignment
99
106
  matching_relation.parent_model_id = args.first.id
100
107
  end
101
108
  when "create_#{target_name}", "create_#{target_name.singularize}"
@@ -1,4 +1,4 @@
1
1
  module PassiveRecord
2
2
  # passive_record version
3
- VERSION = "0.3.0"
3
+ VERSION = "0.3.1"
4
4
  end
@@ -42,11 +42,11 @@ describe "passive record models" do
42
42
 
43
43
  it 'should report relations' do
44
44
  dog = Dog.create
45
- expect(dog.inspect).to eq("Family::Dog (id: #{dog.id.inspect}, created_at: #{dog.created_at}, sound: \"bark\", child_id: nil)")
45
+ expect(dog.inspect).to eq("Family::Dog (id: #{dog.id.inspect}, breed: \"#{dog.breed}\", created_at: #{dog.created_at}, sound: \"bark\", child_id: nil)")
46
46
 
47
47
  child = Child.create
48
48
  child.dogs << dog
49
- expect(dog.inspect).to eq("Family::Dog (id: #{dog.id.inspect}, created_at: #{dog.created_at}, sound: \"bark\", child_id: #{child.id.inspect})")
49
+ expect(dog.inspect).to eq("Family::Dog (id: #{dog.id.inspect}, breed: \"#{dog.breed}\", created_at: #{dog.created_at}, sound: \"bark\", child_id: #{child.id.inspect})")
50
50
 
51
51
  expect(child.inspect).to eq("Family::Child (id: #{child.id.inspect}, created_at: #{dog.created_at}, name: \"Alice\", toy_id: nil, dog_ids: [#{dog.id.inspect}], parent_id: nil)")
52
52
  end
@@ -249,13 +249,33 @@ describe "passive record models" do
249
249
  let(:child) { parent.create_child }
250
250
 
251
251
  it 'should collect children of children' do
252
- child.create_dog
252
+ child.create_dog(breed: 'mutt')
253
253
  expect(parent.dogs.all).to all(be_a(Dog))
254
254
  expect(parent.dogs.count).to eq(1)
255
255
  expect(parent.dogs.first).to eq(child.dogs.first)
256
256
  expect(parent.dog_ids).to eq([child.dogs.first.id])
257
257
  end
258
258
 
259
+ it 'should chain where clauses' do
260
+ child.create_dog(breed: 'mutt')
261
+ child.create_dog(breed: 'pit')
262
+
263
+ # another mutt, not the same childs
264
+ Dog.create(breed: 'mutt')
265
+
266
+ expect(Dog.where(breed: 'mutt').count).to eq(2)
267
+ expect(child.dogs.where(breed: 'mutt').count).to eq(1)
268
+
269
+ expect(
270
+ child.dogs.
271
+ where(breed: 'mutt')
272
+ ).to eq(
273
+ Dog.
274
+ where(child_id: child.id).
275
+ where(breed: 'mutt')
276
+ )
277
+ end
278
+
259
279
  it 'should do the nested query example from the readme' do
260
280
  child.create_dog
261
281
  expect(Dog.find_all_by(child: {parent: parent})).
@@ -312,6 +332,21 @@ describe "passive record models" do
312
332
  expect(user_b.friends.all).to eq([user_a])
313
333
  end
314
334
  end
335
+
336
+ context 'direct habtm' do
337
+ let!(:user) { User.create roles: [role] }
338
+ let(:role) { Role.create }
339
+ let(:another_user) { User.create }
340
+
341
+ it 'should manage direct habtm relations' do
342
+ expect(role.users).to include(user)
343
+ expect(user.roles).to include(role)
344
+ expect(role.user_ids).to eq([user.id])
345
+ expect(user.role_ids).to eq([role.id])
346
+
347
+ expect {role.users << another_user}.to change{role.users.count}.by(1)
348
+ end
349
+ end
315
350
  end
316
351
  end
317
352
 
data/spec/spec_helper.rb CHANGED
@@ -20,8 +20,10 @@ end
20
20
  module Family
21
21
  class Dog < Model
22
22
  attr_reader :sound
23
+ attr_accessor :breed
23
24
  belongs_to :child
24
- after_create {@sound = 'bark'}
25
+ before_create { @breed = %w[pom pug].sample }
26
+ after_create { @sound = 'bark' }
25
27
  end
26
28
 
27
29
  class Toy < Model
@@ -78,6 +80,11 @@ end
78
80
  class User < Model
79
81
  has_many :friendships
80
82
  has_many :friends, :through => :friendships
83
+ has_and_belongs_to_many :roles
84
+ end
85
+
86
+ class Role < Model
87
+ has_and_belongs_to_many :users
81
88
  end
82
89
 
83
90
  ###
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: passive_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joseph Weissman