polymorpheus 1.1.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,25 +1,42 @@
1
+ [![Build Status](https://travis-ci.org/wegowise/polymorpheus.png?branch=master)](https://travis-ci.org/wegowise/polymorpheus)
2
+ [![Code Climate](https://codeclimate.com/github/wegowise/polymorpheus.png)](https://codeclimate.com/github/wegowise/polymorpheus)
3
+
1
4
  # Polymorpheus
2
- **Polymorphic relationships in Rails that keep your database happy with almost no setup**
5
+ **Polymorphic relationships in Rails that keep your database happy with almost
6
+ no setup**
3
7
 
4
8
  ### Background
5
- * **What is polymorphism?** [Rails Guides has a great overview of what polymorphic relationships are and how Rails handles them](http://guides.rubyonrails.org/association_basics.html#polymorphic-associations)
9
+ * **What is polymorphism?** [Rails Guides has a great overview of what
10
+ polymorphic relationships are and how Rails handles them](
11
+ http://guides.rubyonrails.org/association_basics.html#polymorphic-associations)
6
12
 
7
- * **If you don't think database constraints are important** then [here is a presentation that might change your mind](http://bostonrb.org/presentations/databases-constraints-polymorphism). If you're still not convinced, this gem won't be relevant to you.
13
+ * **If you don't think database constraints are important** then [here is a
14
+ presentation that might change your mind](
15
+ http://bostonrb.org/presentations/databases-constraints-polymorphism). If
16
+ you're still not convinced, this gem won't be relevant to you.
8
17
 
9
- * **What's wrong with Rails' built-in approach to polymorphism?** Using Rails, polymorphism is implemented in the database using a `type` column and an `id` column, where the `id` column references one of multiple other tables, depending on the `type`. This violates the basic principle that one column in a database should mean to one thing, and it prevents us from setting up any sort of database constraint on the `id` column.
18
+ * **What's wrong with Rails' built-in approach to polymorphism?** Using Rails,
19
+ polymorphism is implemented in the database using a `type` column and an `id`
20
+ column, where the `id` column references one of multiple other tables,
21
+ depending on the `type`. This violates the basic principle that one column in
22
+ a database should mean to one thing, and it prevents us from setting up any
23
+ sort of database constraint on the `id` column.
10
24
 
11
25
 
12
26
  ## Basic Use
13
27
 
14
- We'll outline the use case to mirror the example [outline in the Rails Guides](http://guides.rubyonrails.org/association_basics.html#polymorphic-associations):
28
+ We'll outline the use case to mirror the example [outline in the Rails Guides](
29
+ http://guides.rubyonrails.org/association_basics.html#polymorphic-associations):
15
30
 
16
- * You have a `Picture` object that can belong to an `Imageable`, where an `Imageable` is a polymorphic representation of either an `Employee` or a `Product`.
31
+ * You have a `Picture` object that can belong to an `Imageable`, where an
32
+ `Imageable` is a polymorphic representation of either an `Employee` or a
33
+ `Product`.
17
34
 
18
35
  With Polymorpheus, you would define this relationship as follows:
19
36
 
20
37
  **Database migration**
21
38
 
22
- ```
39
+ ```ruby
23
40
  class SetUpPicturesTable < ActiveRecord::Migration
24
41
  def self.up
25
42
  create_table :pictures do |t|
@@ -44,18 +61,18 @@ end
44
61
 
45
62
  **ActiveRecord model definitions**
46
63
 
47
- ```
64
+ ```ruby
48
65
  class Picture < ActiveRecord::Base
49
66
  belongs_to_polymorphic :employee, :product, :as => :imageable
50
67
  validates_polymorphic :imageable
51
68
  end
52
69
 
53
70
  class Employee < ActiveRecord::Base
54
- has_many :pictures
71
+ has_many_as_polymorph :pictures
55
72
  end
56
73
 
57
74
  class Product < ActiveRecord::Base
58
- has_many :pictures
75
+ has_many_as_polymorph :pictures
59
76
  end
60
77
  ```
61
78
 
@@ -66,28 +83,57 @@ Now let's review what we've done.
66
83
 
67
84
  ## Database Migration
68
85
 
69
- * Instead of `imageable_type` and `imageable_id` columns in the pictures table, we've created explicit columns for the `employee_id` and `product_id`
70
- * The `add_polymorphic_constraints` call takes care of all of the database constraints you need, without you needing to worry about sql! Specifically it:
71
- * Creates foreign key relationships in the database as specified. So in this example, we have specified that the `employee_id` column in the `pictures` table should have a foreign key constraint with the `id` column of the `employees` table.
72
- * Creates appropriate triggers in our database that make sure that exactly on or the other of `employee_id` or `product_id` are specified for a given record. An exception will be raised if you try to save a database record that contains both or none of them.
73
- * **Options for migrations**: There are options to customize the foreign keys generated by Polymorpheus and add uniqueness constraints. For more info on this, [read the wiki entry](https://github.com/wegowise/polymorpheus/wiki/Migration-options).
86
+ * Instead of `imageable_type` and `imageable_id` columns in the pictures table,
87
+ we've created explicit columns for the `employee_id` and `product_id`
88
+ * The `add_polymorphic_constraints` call takes care of all of the database
89
+ constraints you need, without you needing to worry about sql! Specifically it:
90
+ * Creates foreign key relationships in the database as specified. So in this
91
+ example, we have specified that the `employee_id` column in the `pictures`
92
+ table should have a foreign key constraint with the `id` column of the
93
+ `employees` table.
94
+ * Creates appropriate triggers in our database that make sure that exactly one
95
+ or the other of `employee_id` or `product_id` are specified for a given
96
+ record. An exception will be raised if you try to save a database record
97
+ that contains both or none of them.
98
+ * **Options for migrations**: There are options to customize the foreign keys
99
+ generated by Polymorpheus and add uniqueness constraints. For more info
100
+ on this, [read the wiki entry](https://github.com/wegowise/polymorpheus/wiki/Migration-options).
74
101
 
75
102
  ## Model definitions
76
103
 
77
- * The `belongs_to_polymorphic` declaration in the `Picture` class specifies the polymorphic relationship. It provides all of the same methods that Rails does for it's built-in polymorphic relationships, plus a couple additional features. See the Interface section below.
78
- * `validates_polymorph` declaration: checks that exactly one of the possible polymorphic relationships is specified. In this example, either an `employee_id` or `product_id` must be specified -- if both are nil or if both are non-nil a validation error will be added to the object.
79
- * The `has_many` declarations are just normal Rails declarations.
80
-
104
+ * The `belongs_to_polymorphic` declaration in the `Picture` class specifies the
105
+ polymorphic relationship. It provides all of the same methods that Rails does
106
+ for its built-in polymorphic relationships, plus a couple additional features.
107
+ See the Interface section below.
108
+ * `validates_polymorph` declaration: checks that exactly one of the possible
109
+ polymorphic relationships is specified. In this example, either an
110
+ `employee_id` or `product_id` must be specified -- if both are nil or if both
111
+ are non-nil a validation error will be added to the object.
112
+ * The `has_many_as_polymorph` declaration generates a normal Rails `has_many`
113
+ declaration, but adds a constraint that ensures that the correct records are
114
+ retrieved. This means you can still use the same conditions with it that you
115
+ would use with a `has_many` association (such as `:order`, `:class_name`,
116
+ etc.). Specifically, the `has_many_as_polymorph` declaration in the `Employee`
117
+ class of the example above is equivalant to
118
+ `has_many :pictures, { picture_id: nil }`
119
+ and the `has_many_as_polymorph` declaration in the `Product` class is
120
+ equivalent to `has_many :pictures, { employee_id: nil }`
81
121
 
82
122
  ## Requirements / Support
83
123
 
84
- * Currently the gem only supports MySQL. Postgres support is planned; please feel free to fork and submit a (well-tested) pull request if you want to add Postgres support before then.
85
- * This gem is tested and has been tested for Rails 2.3.8, 3.0.x, 3.1.x and 3.2.x
86
- * For Rails 3.1+, you'll still need to use `up` and `down` methods in your migrations.
124
+ * Currently the gem only supports MySQL. Please feel free to fork and submit a
125
+ (well-tested) pull request if you want to add Postgres support.
126
+ * This gem is tested and has been tested for Rails 2.3.8, 3.0.x, 3.1.x, 3.2.x,
127
+ and 4.0.0
128
+ * For Rails 3.1+, you'll still need to use `up` and `down` methods in your
129
+ migrations.
87
130
 
88
131
  ## Interface
89
132
 
90
- The nice thing about Polymorpheus is that under the hood it builds on top of the Rails conventions you're already used to which means that you can interface with your polymorphic relationships in simple, familiar ways. There are also a number of helpful interface helpers that you can use.
133
+ The nice thing about Polymorpheus is that under the hood it builds on top of the
134
+ Rails conventions you're already used to which means that you can interface with
135
+ your polymorphic relationships in simple, familiar ways. It also lets you
136
+ introspect on the polymorphic associations.
91
137
 
92
138
  Let's use the example above to illustrate.
93
139
 
@@ -101,11 +147,12 @@ pic = Picture.new
101
147
  pic.imageable
102
148
  => nil
103
149
 
104
- # the following two options are equivalent, just as they are normally with ActiveRecord:
150
+ # The following two options are equivalent, just as they are normally with
151
+ # ActiveRecord:
105
152
  # pic.employee = sam
106
153
  # pic.employee_id = sam.id
107
154
 
108
- # if we specify an employee, the imageable getter method will return that employee:
155
+ # If we specify an employee, the imageable getter method will return that employee:
109
156
  pic.employee = sam;
110
157
  pic.imageable
111
158
  => #<Employee id: 1, name: "Sam">
@@ -114,32 +161,43 @@ pic.employee
114
161
  pic.product
115
162
  => nil
116
163
 
117
- # if we specify a product, the imageable getting will return that product:
164
+ # If we specify a product, the imageable getting will return that product:
118
165
  Picture.new(product: nintendo).imageable
119
166
  => #<Product id: 1, name: "Nintendo">
120
167
 
121
- # but if we specify an employee and a product, the getter will know this makes no sense and return nil for the imageable:
168
+ # But, if we specify an employee and a product, the getter will know this makes
169
+ # no sense and return nil for the imageable:
122
170
  Picture.new(employee: sam, product: nintendo).imageable
123
171
  => nil
124
172
 
125
- # There are some useful helper methods:
173
+ # A `polymorpheus` instance method is attached to your model that allows you
174
+ # to introspect:
126
175
 
127
- pic.imageable_active_key
128
- => "employee_id"
176
+ pic.polymorpheus.associations
177
+ => [
178
+ #<Polymorpheus::InterfaceBuilder::Association:0x007f88b5528b00 @name="employee">,
179
+ #<Polymorpheus::InterfaceBuilder::Association:0x007f88b55289c0 @name="picture">
180
+ ]
129
181
 
130
- pic.imageable_query_condition
131
- => {"employee_id"=>"1"}
182
+ pic.polymorpheus.associations.map(&:name)
183
+ => ["employee", "product"]
184
+
185
+ pic.polymorpheus.associations.map(&:key)
186
+ => ["employee_id", "product_id"]
132
187
 
133
- pic.imageable_types
134
- => ["employee", "picture"]
188
+ pic.polymorpheus.active_association
189
+ => #<Polymorpheus::InterfaceBuilder::Association:0x007f88b5528b00 @name="employee">,
135
190
 
136
- Picture::IMAGEABLE_KEYS
137
- => ["employee_id", "picture_id"]
191
+ pic.polymorpheus.query_condition
192
+ => {"employee_id"=>"1"}
138
193
  ```
139
194
 
140
195
  ## Credits and License
141
196
 
142
197
  * This gem was written by [Barun Singh](https://github.com/barunio)
143
- * It uses the [Foreigner gem](https://github.com/matthuhiggins/foreigner) under the hood for a few things
198
+ * It uses the [Foreigner gem](https://github.com/matthuhiggins/foreigner) under
199
+ the hood for a few things
144
200
 
145
- polymorpheus is Copyright © 2011-2012 Barun Singh and [WegoWise](http://wegowise.com). It is free software, and may be redistributed under the terms specified in the LICENSE file.
201
+ polymorpheus is Copyright © 2011-2012 Barun Singh and [WegoWise](
202
+ http://wegowise.com). It is free software, and may be redistributed under the
203
+ terms specified in the LICENSE file.
data/lib/polymorpheus.rb CHANGED
@@ -1,9 +1,14 @@
1
1
  module Polymorpheus
2
2
  autoload :Adapter, 'polymorpheus/adapter'
3
3
  autoload :Interface, 'polymorpheus/interface'
4
+ autoload :InterfaceBuilder, 'polymorpheus/interface_builder'
4
5
  autoload :Trigger, 'polymorpheus/trigger'
5
6
  autoload :SchemaDumper, 'polymorpheus/schema_dumper'
6
7
 
8
+ class InterfaceBuilder
9
+ autoload :Association, 'polymorpheus/interface_builder/association'
10
+ end
11
+
7
12
  module ConnectionAdapters
8
13
  autoload :SchemaStatements, 'polymorpheus/schema_statements'
9
14
  end
@@ -22,81 +22,71 @@ module Polymorpheus
22
22
 
23
23
  module ClassMethods
24
24
 
25
- def belongs_to_polymorphic(*args)
26
- options = args.extract_options!
25
+ def belongs_to_polymorphic(*association_names, options)
27
26
  polymorphic_api = options[:as]
28
- associations = args.collect(&:to_s).collect(&:downcase)
29
- association_keys = associations.collect{|association| "#{association}_id"}
30
-
31
- # Set belongs_to assocaitions
32
- associations.each do |associated_model|
33
- belongs_to associated_model.to_sym
34
- end
35
-
36
- # Class constant defining the keys
37
- const_set "#{polymorphic_api}_keys".upcase, association_keys
38
-
39
- # Helper methods and constants
40
- define_method "#{polymorphic_api}_types" do
41
- associations
27
+ builder = Polymorpheus::InterfaceBuilder.new(polymorphic_api,
28
+ association_names)
29
+
30
+ # The POLYMORPHEUS_ASSOCIATIONS constant is useful for two reasons:
31
+ #
32
+ # 1. It is useful for other classes to be able to ask this class
33
+ # about its polymorphic relationship.
34
+ #
35
+ # 2. It prevents a class from defining multiple polymorphic
36
+ # relationships. Doing so would be a bad idea from a design
37
+ # standpoint, and we don't want to allow for (and support)
38
+ # that added complexity.
39
+ #
40
+ const_set('POLYMORPHEUS_ASSOCIATIONS', builder.association_names)
41
+
42
+ # Set belongs_to associations
43
+ builder.associations.each do |association|
44
+ belongs_to association.name.to_sym
42
45
  end
43
46
 
44
- define_method "#{polymorphic_api}_active_key" do
45
- keys = association_keys.select { |key| self.send(key).present? }
46
- keys.first if keys.length == 1
47
- end
48
-
49
- define_method "#{polymorphic_api}_query_condition" do
50
- fk = self.send("#{polymorphic_api}_active_key")
51
- { fk.to_s => self.send(fk) } if fk
47
+ # Exposed interface for introspection
48
+ define_method 'polymorpheus' do
49
+ builder.exposed_interface(self)
52
50
  end
53
51
 
54
52
  # Getter method
55
53
  define_method polymorphic_api do
56
- if key = self.send("#{polymorphic_api}_active_key")
57
- # we are connecting to an existing item in the db
58
- self.send key.gsub(/_id$/,'')
59
- else
60
- # we can also link to a new record if we're careful
61
- objs = associations.map { |association| self.send(association) }.compact
62
- objs.first if objs.length == 1
63
- end
54
+ builder.get_associated_object(self)
64
55
  end
65
56
 
66
57
  # Setter method
67
- define_method "#{polymorphic_api}=" do |polymorphic_obj|
68
-
69
- klass_ancestors = polymorphic_obj.class
70
- .ancestors.map(&:name).compact.map(&:underscore)
71
- match = associations & klass_ancestors
72
-
73
- if match.blank?
74
- raise Polymorpheus::Interface::InvalidTypeError, associations
75
- elsif match.length > 1
76
- raise Polymorpheus::Interface::AmbiguousTypeError
77
- else
78
- accessor = "#{polymorphic_obj.class.base_class.name.underscore}_id="
79
- self.send(accessor, polymorphic_obj.id)
80
- (associations - match).each do |association_to_reset|
81
- self.send("#{association_to_reset}_id=", nil)
82
- end
83
- end
58
+ define_method "#{polymorphic_api}=" do |object_to_associate|
59
+ builder.set_associated_object(self, object_to_associate)
84
60
  end
61
+ end
85
62
 
86
- # Private method called as part of validation
87
- # Validate that there is exactly one associated object
88
- define_method "polymorphic_#{polymorphic_api}_relationship_is_valid" do
89
- if !self.send(polymorphic_api)
90
- self.errors.add(:base,
91
- "You must specify exactly one of the following: {#{associations.join(', ')}}")
92
- end
93
- end
94
- private "polymorphic_#{polymorphic_api}_relationship_is_valid"
63
+ def has_many_as_polymorph(association, options = {})
64
+ options.symbolize_keys!
65
+ conditions = options.fetch(:conditions, {})
66
+ fkey = name.foreign_key
67
+
68
+ class_name = options[:class_name] || association.to_s.classify
95
69
 
70
+ options[:conditions] = proc {
71
+ keys = class_name.constantize
72
+ .const_get('POLYMORPHEUS_ASSOCIATIONS')
73
+ .map(&:foreign_key)
74
+ keys.delete(fkey)
75
+
76
+ keys.reduce({}) { |hash, key| hash.merge!(key => nil) }
77
+ }
78
+
79
+ has_many association, options
96
80
  end
97
81
 
98
82
  def validates_polymorph(polymorphic_api)
99
- validate "polymorphic_#{polymorphic_api}_relationship_is_valid"
83
+ validate Proc.new {
84
+ unless polymorpheus.active_association
85
+ association_names = polymorpheus.associations.map(&:name)
86
+ errors.add(:base, "You must specify exactly one of the following: "\
87
+ "{#{association_names.join(', ')}}")
88
+ end
89
+ }
100
90
  end
101
91
 
102
92
  end
@@ -0,0 +1,93 @@
1
+ require 'ostruct'
2
+
3
+ module Polymorpheus
4
+ class InterfaceBuilder
5
+
6
+ attr_reader :interface_name,
7
+ :associations
8
+
9
+ def initialize(interface_name, association_names)
10
+ @interface_name = interface_name
11
+ @associations = association_names.map do |association_name|
12
+ Polymorpheus::InterfaceBuilder::Association.new(association_name)
13
+ end
14
+ end
15
+
16
+ def exposed_interface(calling_object)
17
+ OpenStruct.new(
18
+ associations: associations,
19
+ active_association: active_association(calling_object),
20
+ query_condition: query_condition(calling_object)
21
+ )
22
+ end
23
+
24
+ def association_keys
25
+ @association_keys ||= associations.map(&:key)
26
+ end
27
+
28
+ def association_names
29
+ @association_names ||= associations.map(&:name)
30
+ end
31
+
32
+ def active_association(calling_object)
33
+ active_associations = associations.select do |association|
34
+ # If the calling object has a non-nil value for the association
35
+ # key, we know it has an active associatin without having to
36
+ # make a database query to retrieve the associated object itself.
37
+ #
38
+ # If it has a nil value for the association key, we then ask if
39
+ # it has a non-nil result for the association itself, since it
40
+ # may have an active association that has not yet been saved to
41
+ # the database.
42
+ #
43
+ calling_object.public_send(association.key).present? ||
44
+ calling_object.public_send(association.name).present?
45
+ end
46
+
47
+ active_associations.first if active_associations.length == 1
48
+ end
49
+
50
+ def active_association_key(calling_object)
51
+ association = active_association(calling_object)
52
+ return unless association
53
+
54
+ association.key if calling_object.public_send(association.key)
55
+ end
56
+
57
+ def query_condition(calling_object)
58
+ key = active_association_key(calling_object)
59
+ object = calling_object.public_send(key) if key
60
+
61
+ { key.to_s => object } if object
62
+ end
63
+
64
+ def get_associated_object(calling_object)
65
+ association = active_association(calling_object)
66
+ calling_object.public_send(association.name) if association
67
+ end
68
+
69
+ def set_associated_object(calling_object, object_to_associate)
70
+ association = get_relevant_association_for_object(object_to_associate)
71
+ calling_object.public_send("#{association.name}=", object_to_associate)
72
+
73
+ (associations - [association]).each do |association|
74
+ calling_object.public_send("#{association.name}=", nil)
75
+ end
76
+ end
77
+
78
+ def get_relevant_association_for_object(object_to_associate)
79
+ match = associations.select do |association|
80
+ object_to_associate.is_a?(association.association_class)
81
+ end
82
+
83
+ if match.blank?
84
+ raise Polymorpheus::Interface::InvalidTypeError, association_names
85
+ elsif match.length > 1
86
+ raise Polymorpheus::Interface::AmbiguousTypeError
87
+ end
88
+
89
+ match.first
90
+ end
91
+
92
+ end
93
+ end
@@ -0,0 +1,23 @@
1
+ module Polymorpheus
2
+ class InterfaceBuilder
3
+ class Association
4
+
5
+ include ActiveSupport::Inflector
6
+
7
+ attr_reader :name,
8
+ :key
9
+
10
+ def initialize(name)
11
+ @name = name.to_s.downcase
12
+ @key = "#{@name}_id"
13
+ end
14
+
15
+ # The association class may not be loaded at the time this object
16
+ # is initialized, so we can't set it via an accessor in the initializer.
17
+ def association_class
18
+ @association_class ||= name.classify.constantize
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
1
  module Polymorpheus
2
- VERSION = '1.1.3'
2
+ VERSION = '2.0.0'
3
3
  end
@@ -11,16 +11,18 @@ class Shoe < ActiveRecord::Base
11
11
  end
12
12
 
13
13
  class Man < ActiveRecord::Base
14
+ has_many_as_polymorph :shoes
14
15
  end
15
16
 
16
17
  class Woman < ActiveRecord::Base
18
+ has_many_as_polymorph :shoes, order: 'id DESC'
17
19
  end
18
20
 
19
21
  class Dog < ActiveRecord::Base
20
22
  end
21
23
 
22
24
  class Glove < ActiveRecord::Base
23
- belongs_to_polymorphic :gentleman, :as => :wearer
25
+ belongs_to_polymorphic :gentleman, :gentlewoman, :as => :wearer
24
26
  validates_polymorph :wearer
25
27
  end
26
28
 
@@ -30,26 +32,47 @@ end
30
32
  class Knight < Gentleman
31
33
  end
32
34
 
33
- describe Shoe do
34
-
35
- let(:shoe) { Shoe.new(attributes) }
36
- let(:man) { Man.create! }
37
- let(:woman) { Woman.create! }
35
+ class Gentlewoman < Woman
36
+ end
38
37
 
39
- describe "class level constant" do
40
- specify { Shoe::WEARER_KEYS.should == ["man_id", "woman_id"] }
38
+ describe '.belongs_to_polymorphic' do
39
+ it 'sets conditions on association to ensure we retrieve correct result' do
40
+ man = Man.create!
41
+ man.shoes.to_sql.squish
42
+ .should == %{SELECT `shoes`.* FROM `shoes`
43
+ WHERE `shoes`.`man_id` = 1
44
+ AND `shoes`.`woman_id` IS NULL}.squish
41
45
  end
42
46
 
43
- describe "helper methods" do
44
- specify { Shoe.new.wearer_types.should == ["man", "woman"] }
47
+ it 'supports existing conditions on the association' do
48
+ woman = Woman.create!
49
+ woman.shoes.to_sql.squish
50
+ .should == %{SELECT `shoes`.* FROM `shoes`
51
+ WHERE `shoes`.`woman_id` = 1
52
+ AND `shoes`.`man_id` IS NULL
53
+ ORDER BY id DESC}.squish
45
54
  end
46
55
 
47
- it "make the dynamically defined validation method private" do
48
- Shoe.private_instance_methods.
49
- include?(:polymorphic_wearer_relationship_is_valid).should be_true
56
+ it 'returns the correct result when used with new records' do
57
+ woman = Woman.create!
58
+ shoe = Shoe.create!(woman: woman, other_id: 10)
59
+ Man.new.shoes.where(other_id: 10).should == []
50
60
  end
61
+ end
62
+
63
+ describe "polymorphic interface" do
64
+
65
+ let(:man) { Man.create! }
66
+ let(:woman) { Woman.create! }
67
+ let(:gentleman) { Gentleman.create! }
68
+ let(:knight) { Knight.create! }
51
69
 
52
- describe "dynamic generated setter" do
70
+ specify { Shoe::POLYMORPHEUS_ASSOCIATIONS.should == %w[man woman] }
71
+ specify { Glove::POLYMORPHEUS_ASSOCIATIONS.should == %w[gentleman
72
+ gentlewoman] }
73
+
74
+ describe "setter methods for ActiveRecord objects" do
75
+ let(:shoe) { Shoe.new(attributes) }
53
76
  let(:attributes) { {} }
54
77
 
55
78
  it "sets the correct attribute value for the setter" do
@@ -73,76 +96,105 @@ describe Shoe do
73
96
  "Invalid type. Must be one of {man, woman}")
74
97
  end
75
98
 
76
- it "does not throw an error if the assigned object is a subclass of a valid type" do
77
- gentleman = Gentleman.create!
99
+ it "does not throw an error if the assigned object is a subclass of a
100
+ valid type" do
78
101
  expect { shoe.wearer = gentleman }.not_to raise_error
79
102
  shoe.man_id.should == gentleman.id
80
103
  end
81
104
 
82
- it "does not throw an error if the assigned object is a descendant of a valid type" do
83
- knight = Knight.create!
105
+ it "does not throw an error if the assigned object is a descendant of a
106
+ valid type" do
84
107
  expect { shoe.wearer = knight }.not_to raise_error
85
108
  shoe.man_id.should == knight.id
86
109
  end
110
+ end
87
111
 
88
- it "does not throw an error if the association is to a parent class of
89
- assigned object that is not the base" do
90
- glove = Glove.new({})
91
- knight = Knight.create!
92
- expect { glove.wearer = knight }.not_to raise_error
93
- glove.man_id.should == knight.id
112
+ describe "setter methods for objects inheriting from ActiveRecord objects" do
113
+ let(:glove) { Glove.new }
114
+
115
+ it "throws an error if the assigned object is an instance of the parent
116
+ ActiveRecord class" do
117
+ expect { glove.wearer = man }.to raise_error(
118
+ Polymorpheus::Interface::InvalidTypeError,
119
+ "Invalid type. Must be one of {gentleman, gentlewoman}"
120
+ )
94
121
  end
95
122
 
96
- it "throws an error if assigned object is a less specific instance of the association" do
97
- glove = Glove.new({})
98
- man = Man.create!
99
- expect { glove.wearer = man }
100
- .to raise_error(Polymorpheus::Interface::InvalidTypeError,
101
- "Invalid type. Must be one of {gentleman}")
123
+ it "works if the assigned object is of the specified class" do
124
+ expect { glove.wearer = gentleman }.not_to raise_error
125
+ glove.gentleman_id.should == gentleman.id
126
+ end
127
+
128
+ it "works if the assigned object is an instance of a child class" do
129
+ expect { glove.wearer = knight }.not_to raise_error
130
+ glove.gentleman_id.should == knight.id
102
131
  end
103
132
  end
104
133
 
105
- shared_examples_for "invalid polymorphic relationship" do
106
- specify { shoe.wearer.should == nil }
107
- specify { shoe.wearer_active_key.should == nil }
108
- specify { shoe.wearer_query_condition.should == nil }
109
- it "validates appropriately" do
110
- shoe.valid?.should be_false
134
+ describe '.validates_polymorph validation' do
135
+ specify { Shoe.new(wearer: man).valid?.should == true }
136
+ specify { Shoe.new(wearer: woman).valid?.should == true }
137
+ specify { Shoe.new(man_id: man.id).valid?.should == true }
138
+ specify { Shoe.new(man: man).valid?.should == true }
139
+ specify { Shoe.new(man: Man.new).valid?.should == true }
140
+
141
+ it 'is invalid if no association is specified' do
142
+ shoe = Shoe.new
143
+ shoe.valid?.should == false
111
144
  shoe.errors[:base].should ==
112
145
  ["You must specify exactly one of the following: {man, woman}"]
113
146
  end
114
- end
115
147
 
116
- context "when there is no relationship defined" do
117
- let(:attributes) { {} }
118
- it_should_behave_like "invalid polymorphic relationship"
148
+ it 'is invalid if multiple associations are specified' do
149
+ shoe = Shoe.new(man_id: man.id, woman_id: woman.id)
150
+ shoe.valid?.should == false
151
+ shoe.errors[:base].should ==
152
+ ["You must specify exactly one of the following: {man, woman}"]
153
+ end
119
154
  end
120
155
 
121
- context "when there are multiple relationships defined" do
122
- let(:attributes) { { man_id: man.id, woman_id: woman.id } }
123
- it_should_behave_like "invalid polymorphic relationship"
124
- end
156
+ describe '#polymorpheus exposed interface method' do
157
+ subject(:interface) { shoe.polymorpheus }
125
158
 
126
- context "when there is exactly one relationship defined" do
127
- shared_examples_for "valid polymorphic relationship" do
128
- specify { shoe.wearer.should == man }
129
- specify { shoe.wearer_active_key.should == 'man_id' }
130
- specify { shoe.wearer_query_condition.should == { 'man_id' => man.id } }
159
+ context 'when there is no relationship defined' do
160
+ let(:shoe) { Shoe.new }
161
+
162
+ its(:associations) { should match_associations(:man, :woman) }
163
+ its(:active_association) { should == nil }
164
+ its(:query_condition) { should == nil }
131
165
  end
132
- context "and we have specified it via the id value" do
133
- let(:attributes) { { man_id: man.id } }
134
- it_should_behave_like "valid polymorphic relationship"
166
+
167
+ context 'when there is are multiple relationships defined' do
168
+ let(:shoe) { Shoe.new(man_id: man.id, woman_id: woman.id) }
169
+
170
+ its(:associations) { should match_associations(:man, :woman) }
171
+ its(:active_association) { should == nil }
172
+ its(:query_condition) { should == nil }
173
+ end
174
+
175
+ context 'when there is one relationship defined through the id value' do
176
+ let(:shoe) { Shoe.new(man_id: man.id) }
177
+
178
+ its(:associations) { should match_associations(:man, :woman) }
179
+ its(:active_association) { be_association(:man) }
180
+ its(:query_condition) { should == { 'man_id' => man.id } }
135
181
  end
136
- context "and we have specified it via the id value" do
137
- let(:attributes) { { man: man } }
138
- it_should_behave_like "valid polymorphic relationship"
182
+
183
+ context 'when there is one relationship defined through the setter' do
184
+ let(:shoe) { Shoe.new(wearer: man) }
185
+
186
+ its(:associations) { should match_associations(:man, :woman) }
187
+ its(:active_association) { be_association(:man) }
188
+ its(:query_condition) { should == { 'man_id' => man.id } }
139
189
  end
140
- context "and the record we are linking to is a new record" do
190
+
191
+ context 'when there is one association, to a new record' do
141
192
  let(:new_man) { Man.new }
142
- let(:attributes) { { man: new_man } }
143
- specify { shoe.wearer.should == new_man }
144
- specify { shoe.wearer_active_key.should be_nil }
145
- specify { shoe.wearer_query_condition.should be_nil }
193
+ let(:shoe) { Shoe.new(wearer: new_man) }
194
+
195
+ its(:associations) { should match_associations(:man, :woman) }
196
+ its(:active_association) { be_association(:man) }
197
+ its(:query_condition) { should == nil }
146
198
  end
147
199
  end
148
200
 
data/spec/spec_helper.rb CHANGED
@@ -14,10 +14,11 @@ ActiveRecord::Schema.define do
14
14
  create_table :shoes do |t|
15
15
  t.integer :man_id
16
16
  t.integer :woman_id
17
+ t.integer :other_id
17
18
  end
18
19
  create_table :gloves do |t|
19
- t.integer :man_id
20
- t.integer :woman_id
20
+ t.integer :gentleman_id
21
+ t.integer :gentlewoman_id
21
22
  end
22
23
  create_table :men do |t|
23
24
  t.string :type
@@ -26,3 +27,19 @@ ActiveRecord::Schema.define do
26
27
  create_table :dogs
27
28
  end
28
29
 
30
+
31
+ RSpec::Matchers.define :be_association do |association_name|
32
+ match do |actual|
33
+ actual.should be_instance_of(Polymorpheus::InterfaceBuilder::Association)
34
+ actual.name.should == association_name.to_s
35
+ end
36
+ end
37
+
38
+ RSpec::Matchers.define :match_associations do |*association_names|
39
+ match do |actual|
40
+ actual.length.should == association_names.length
41
+ actual.each_with_index do |item, ind|
42
+ item.should be_association(association_names[ind])
43
+ end
44
+ end
45
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: polymorpheus
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.3
4
+ version: 2.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-06-10 00:00:00.000000000 Z
12
+ date: 2013-09-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: foreigner
@@ -85,6 +85,8 @@ extra_rdoc_files:
85
85
  files:
86
86
  - lib/polymorpheus/adapter.rb
87
87
  - lib/polymorpheus/interface.rb
88
+ - lib/polymorpheus/interface_builder/association.rb
89
+ - lib/polymorpheus/interface_builder.rb
88
90
  - lib/polymorpheus/mysql_adapter.rb
89
91
  - lib/polymorpheus/railtie.rb
90
92
  - lib/polymorpheus/schema_dumper.rb
@@ -124,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
124
126
  version: 1.3.6
125
127
  requirements: []
126
128
  rubyforge_project:
127
- rubygems_version: 1.8.23
129
+ rubygems_version: 1.8.25
128
130
  signing_key:
129
131
  specification_version: 3
130
132
  summary: Provides a database-friendly method for polymorphic relationships