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 +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
|
+
[![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
|
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
|