polymorpheus 1.1.3 → 2.0.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.
- data/README.md +96 -38
- data/lib/polymorpheus.rb +5 -0
- data/lib/polymorpheus/interface.rb +49 -59
- data/lib/polymorpheus/interface_builder.rb +93 -0
- data/lib/polymorpheus/interface_builder/association.rb +23 -0
- data/lib/polymorpheus/version.rb +1 -1
- data/spec/interface_spec.rb +112 -60
- data/spec/spec_helper.rb +19 -2
- metadata +5 -3
data/README.md
CHANGED
|
@@ -1,25 +1,42 @@
|
|
|
1
|
+
[](https://travis-ci.org/wegowise/polymorpheus)
|
|
2
|
+
[](https://codeclimate.com/github/wegowise/polymorpheus)
|
|
3
|
+
|
|
1
4
|
# Polymorpheus
|
|
2
|
-
**Polymorphic relationships in Rails that keep your database happy with almost
|
|
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
|
|
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
|
|
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,
|
|
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](
|
|
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
|
|
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
|
-
|
|
71
|
+
has_many_as_polymorph :pictures
|
|
55
72
|
end
|
|
56
73
|
|
|
57
74
|
class Product < ActiveRecord::Base
|
|
58
|
-
|
|
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,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
*
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
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.
|
|
85
|
-
|
|
86
|
-
*
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
173
|
+
# A `polymorpheus` instance method is attached to your model that allows you
|
|
174
|
+
# to introspect:
|
|
126
175
|
|
|
127
|
-
pic.
|
|
128
|
-
=>
|
|
176
|
+
pic.polymorpheus.associations
|
|
177
|
+
=> [
|
|
178
|
+
#<Polymorpheus::InterfaceBuilder::Association:0x007f88b5528b00 @name="employee">,
|
|
179
|
+
#<Polymorpheus::InterfaceBuilder::Association:0x007f88b55289c0 @name="picture">
|
|
180
|
+
]
|
|
129
181
|
|
|
130
|
-
pic.
|
|
131
|
-
=>
|
|
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.
|
|
134
|
-
=>
|
|
188
|
+
pic.polymorpheus.active_association
|
|
189
|
+
=> #<Polymorpheus::InterfaceBuilder::Association:0x007f88b5528b00 @name="employee">,
|
|
135
190
|
|
|
136
|
-
|
|
137
|
-
=>
|
|
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
|
|
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](
|
|
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(*
|
|
26
|
-
options = args.extract_options!
|
|
25
|
+
def belongs_to_polymorphic(*association_names, options)
|
|
27
26
|
polymorphic_api = options[:as]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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 |
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
data/lib/polymorpheus/version.rb
CHANGED
data/spec/interface_spec.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
77
|
-
|
|
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
|
|
83
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
glove.
|
|
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 "
|
|
97
|
-
glove =
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
specify {
|
|
107
|
-
specify {
|
|
108
|
-
specify {
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
190
|
+
|
|
191
|
+
context 'when there is one association, to a new record' do
|
|
141
192
|
let(:new_man) { Man.new }
|
|
142
|
-
let(:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 :
|
|
20
|
-
t.integer :
|
|
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:
|
|
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-
|
|
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.
|
|
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
|