polymorpheus 2.2.0 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 2b4fa428b1261c05072970feddd72eca1d4e0d70
4
- data.tar.gz: 189801e622b209426bb6e39b86a32b7c34bbc954
2
+ SHA256:
3
+ metadata.gz: 4ab6587aad06136dbdf95584bba576349fc9731187a60dd6945d74bcb400b025
4
+ data.tar.gz: b83008d22a38740f1ef2909971e5fd662ed8ac5297449ddd1dd405af43a4d792
5
5
  SHA512:
6
- metadata.gz: 1624b4a33ce2a7875acda4688ebad9662e2f6201e01d329196cee018acfcbec0bafe86f3d07c732c383525e13b96e3fda65b510f29201615aa7637803300b32c
7
- data.tar.gz: f207481aef2d8e20c22c7efb2d86691c2d9ed6ba241356e7eb977a27c3d7c90c1f9aceab5f3b4972956f730ca0baa850d6c5b3e0a38866f54a478e6b8daaeb29
6
+ metadata.gz: 476c715b239796573c7a2de566a4dd52507445c68227c4559cb7ec9a443ab707f48d31ad1477edfcfa52183e18634eac329bbe7d7c4bdbf006d620544c9d88b8
7
+ data.tar.gz: a778788d64292837ad5ed6e4a4a4b4fbc9e2fcdd5f38d1c99088c3fa2e7d838b07e20e1df3cae0cb0c7bf6c9e537c0c1d90c3c1ac17fcd8b5db2076def0194b0
data/README.md CHANGED
@@ -5,6 +5,23 @@
5
5
  **Polymorphic relationships in Rails that keep your database happy with almost
6
6
  no setup**
7
7
 
8
+ ### Installation
9
+
10
+ If you are using Bundler, you can add the gem to your Gemfile:
11
+
12
+ ```ruby
13
+ # with Rails >= 4.2
14
+ gem 'polymorpheus'
15
+ ```
16
+
17
+ Or:
18
+
19
+ ```ruby
20
+ # with Rails < 4.2
21
+ gem 'foreigner'
22
+ gem 'polymorpheus'
23
+ ```
24
+
8
25
  ### Background
9
26
  * **What is polymorphism?** [Rails Guides has a great overview of what
10
27
  polymorphic relationships are and how Rails handles them](
@@ -63,12 +80,14 @@ end
63
80
 
64
81
  ```ruby
65
82
  class Picture < ActiveRecord::Base
83
+ # takes same additional options as belongs_to
66
84
  belongs_to_polymorphic :employee, :product, :as => :imageable
67
85
  validates_polymorph :imageable
68
86
  end
69
87
 
70
88
  class Employee < ActiveRecord::Base
71
- has_many_as_polymorph :pictures
89
+ # takes same additional options as has_many
90
+ has_many_as_polymorph :pictures, inverse_of: employee
72
91
  end
73
92
 
74
93
  class Product < ActiveRecord::Base
@@ -123,8 +142,6 @@ Now let's review what we've done.
123
142
 
124
143
  * Currently the gem only supports MySQL. Please feel free to fork and submit a
125
144
  (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
145
  * For Rails 3.1+, you'll still need to use `up` and `down` methods in your
129
146
  migrations.
130
147
 
@@ -196,7 +213,7 @@ pic.polymorpheus.query_condition
196
213
 
197
214
  * This gem was written by [Barun Singh](https://github.com/barunio)
198
215
  * It uses the [Foreigner gem](https://github.com/matthuhiggins/foreigner) under
199
- the hood for a few things
216
+ the hood for Rails < 4.2.
200
217
 
201
218
  polymorpheus is Copyright © 2011-2015 Barun Singh and [WegoWise](
202
219
  http://wegowise.com). It is free software, and may be redistributed under the
@@ -23,8 +23,16 @@ module Polymorpheus
23
23
  module ConnectionAdapters
24
24
  autoload :SchemaStatements, 'polymorpheus/schema_statements'
25
25
  end
26
+
27
+ def self.require_foreigner?
28
+ ActiveRecord::VERSION::MAJOR < 5 &&
29
+ !(::ActiveRecord::VERSION::MAJOR >= 4 &&
30
+ ::ActiveRecord::VERSION::MINOR >= 2)
31
+ end
26
32
  end
27
33
 
34
+ require 'foreigner' if ::Polymorpheus.require_foreigner?
35
+
28
36
  Polymorpheus::Adapter.register 'mysql2', 'polymorpheus/mysql_adapter'
29
37
  Polymorpheus::Adapter.register 'postgresql', 'polymorpheus/postgresql_adapter'
30
38
 
@@ -2,9 +2,10 @@ module Polymorpheus
2
2
  module Interface
3
3
  module BelongsToPolymorphic
4
4
  def belongs_to_polymorphic(*association_names, options)
5
- polymorphic_api = options[:as]
5
+ polymorphic_api = options.delete(:as)
6
6
  builder = Polymorpheus::InterfaceBuilder.new(polymorphic_api,
7
- association_names)
7
+ association_names,
8
+ options)
8
9
 
9
10
  # The POLYMORPHEUS_ASSOCIATIONS constant is useful for two reasons:
10
11
  #
@@ -20,7 +21,7 @@ module Polymorpheus
20
21
 
21
22
  # Set belongs_to associations
22
23
  builder.associations.each do |association|
23
- belongs_to association.name.to_sym
24
+ belongs_to association.name.to_sym, association.options
24
25
  end
25
26
 
26
27
  # Exposed interface for introspection
@@ -6,10 +6,10 @@ module Polymorpheus
6
6
  attr_reader :interface_name,
7
7
  :associations
8
8
 
9
- def initialize(interface_name, association_names)
9
+ def initialize(interface_name, association_names, options)
10
10
  @interface_name = interface_name
11
11
  @associations = association_names.map do |association_name|
12
- Polymorpheus::InterfaceBuilder::Association.new(association_name)
12
+ Polymorpheus::InterfaceBuilder::Association.new(association_name, options)
13
13
  end
14
14
  end
15
15
 
@@ -5,10 +5,11 @@ module Polymorpheus
5
5
  include ActiveSupport::Inflector
6
6
 
7
7
  attr_reader :name,
8
- :key
8
+ :key,
9
+ :options
9
10
 
10
- def initialize(name)
11
- @name = name.to_s.downcase
11
+ def initialize(name, options)
12
+ @name, @options = name.to_s.downcase, options
12
13
  @key = "#{@name}_id"
13
14
  end
14
15
 
@@ -66,19 +66,22 @@ module Polymorpheus
66
66
  if options[:unique].present?
67
67
  poly_create_indexes(table, column_names, Array(options[:unique]))
68
68
  end
69
+
69
70
  column_names.each do |col_name|
70
71
  ref_table, ref_col = columns[col_name].to_s.split('.')
71
- add_foreign_key table, ref_table,
72
- :column => col_name,
73
- :primary_key => (ref_col || 'id'),
74
- :options => generate_constraints(options)
72
+ fk_options = {
73
+ :column => col_name,
74
+ :name => "#{table}_#{col_name}_fk",
75
+ :primary_key => (ref_col || 'id' )
76
+ }.merge(generate_constraints(options))
77
+ add_foreign_key(table, ref_table, fk_options)
75
78
  end
76
79
  end
77
80
 
78
81
  def remove_polymorphic_constraints(table, columns, options = {})
79
82
  poly_drop_triggers(table, columns.keys.sort)
80
83
  columns.each do |(col, reference)|
81
- remove_foreign_key table, :column => col
84
+ remove_foreign_key table, :column => col, :name => "#{table}_#{col}_fk"
82
85
  end
83
86
  if options[:unique].present?
84
87
  poly_remove_indexes(table, columns.keys, Array(options[:unique]))
@@ -86,7 +89,7 @@ module Polymorpheus
86
89
  end
87
90
 
88
91
  def triggers
89
- execute("show triggers").collect {|t| Trigger.new(t) }
92
+ execute("show triggers").collect {|t| Polymorpheus::Trigger.new(t) }
90
93
  end
91
94
 
92
95
  #
@@ -191,26 +194,8 @@ module Polymorpheus
191
194
  end
192
195
 
193
196
  def generate_constraints(options)
194
- constraints = []
195
-
196
- ['delete', 'update'].each do |event|
197
- option = "on_#{event}".to_sym
198
- next unless options.has_key?(option) &&
199
- options[option].respond_to?(:to_sym)
200
-
201
- action = case options[option].to_sym
202
- when :nullify then 'SET NULL'
203
- when :cascade then 'CASCADE'
204
- when :restrict then 'RESTRICT'
205
- end
206
- next unless action
207
-
208
- constraints << "ON #{event.upcase} #{action}"
209
- end
210
-
211
- constraints.join(' ')
197
+ options.slice(:on_delete, :on_update)
212
198
  end
213
-
214
199
  end
215
200
  end
216
201
  end
@@ -223,3 +208,8 @@ end
223
208
  rescue
224
209
  end
225
210
  end
211
+
212
+ if ::Polymorpheus.require_foreigner?
213
+ require 'foreigner/connection_adapters/mysql2_adapter'
214
+ require 'polymorpheus/mysql_adapter/foreigner_constraints'
215
+ end
@@ -0,0 +1,30 @@
1
+ module Polymorpheus
2
+ module ConnectionAdapters
3
+ module MysqlAdapter
4
+ def generate_constraints(options)
5
+ constraints = []
6
+
7
+ ['delete', 'update'].each do |event|
8
+ option = "on_#{event}".to_sym
9
+ next unless options.has_key?(option) &&
10
+ options[option].respond_to?(:to_sym)
11
+
12
+ action = case options[option].to_sym
13
+ when :nullify then 'SET NULL'
14
+ when :cascade then 'CASCADE'
15
+ when :restrict then 'RESTRICT'
16
+ else
17
+ fail ArgumentError, <<-EOS
18
+ '#{options[option]}' is not supported for :on_update or :on_delete.
19
+ Supported values are: :nullify, :cascade, :restrict
20
+ EOS
21
+ end
22
+
23
+ constraints << "ON #{event.upcase} #{action}"
24
+ end
25
+
26
+ { :options => constraints.join(' ') }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -3,14 +3,15 @@ module Polymorpheus
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- alias_method_chain :tables, :triggers
6
+ alias_method :tables_without_triggers, :tables
7
+ alias_method :tables, :tables_with_triggers
7
8
  end
8
9
 
9
10
  def tables_with_triggers(stream)
10
11
  tables_without_triggers(stream)
11
12
 
12
13
  if @connection.respond_to?(:triggers)
13
- @connection.triggers.collect(&:schema_statement).each do |statement|
14
+ @connection.triggers.collect(&:schema_statement).uniq.each do |statement|
14
15
  stream.puts statement
15
16
  end
16
17
  end
@@ -1,29 +1,30 @@
1
- class Trigger
1
+ module Polymorpheus
2
+ class Trigger
2
3
 
3
- attr_accessor :name, :event, :table, :statement, :timing, :created, :sql_mode,
4
- :definer, :charset, :collation_connection, :db_collation
4
+ attr_accessor :name, :event, :table, :statement, :timing, :created, :sql_mode,
5
+ :definer, :charset, :collation_connection, :db_collation
5
6
 
6
- def initialize(arr)
7
- raise ArgumentError unless arr.is_a?(Array) && arr.length == 11
8
- [:name, :event, :table, :statement, :timing, :created, :sql_mode,
9
- :definer, :charset, :collation_connection, :db_collation].
10
- each_with_index do |attr, ind|
11
- self.send("#{attr}=", arr[ind])
7
+ def initialize(arr)
8
+ raise ArgumentError unless arr.is_a?(Array) && arr.length == 11
9
+ [:name, :event, :table, :statement, :timing, :created, :sql_mode,
10
+ :definer, :charset, :collation_connection, :db_collation].
11
+ each_with_index do |attr, ind|
12
+ self.send("#{attr}=", arr[ind])
13
+ end
12
14
  end
13
- end
14
15
 
15
- def columns
16
- /IF\((.*)\) \<\> 1/.match(self.statement) do |match|
17
- match[1].split(' + ').collect do |submatch|
18
- /NEW\.([^ ]*)/.match(submatch)[1]
16
+ def columns
17
+ /IF\((.*)\) \<\> 1/.match(self.statement) do |match|
18
+ match[1].split(' + ').collect do |submatch|
19
+ /NEW\.([^ ]*)/.match(submatch)[1]
20
+ end
19
21
  end
20
22
  end
21
- end
22
23
 
23
- def schema_statement
24
- # note that we don't need to worry about unique indices or foreign keys
25
- # because separate schema statements will be generated for them
26
- " add_polymorphic_triggers(:#{table}, #{columns.to_s})"
24
+ def schema_statement
25
+ # note that we don't need to worry about unique indices or foreign keys
26
+ # because separate schema statements will be generated for them
27
+ " add_polymorphic_triggers(:#{table}, #{columns.to_s})"
28
+ end
27
29
  end
28
-
29
30
  end
@@ -1,3 +1,3 @@
1
1
  module Polymorpheus
2
- VERSION = '2.2.0'
2
+ VERSION = '3.3.0'
3
3
  end
@@ -17,10 +17,8 @@ Gem::Specification.new do |s|
17
17
  s.extra_rdoc_files = ["README.md", "LICENSE.txt"]
18
18
  s.license = 'MIT'
19
19
 
20
- s.add_dependency('foreigner')
21
- s.add_dependency('activerecord', '>= 3.2', '< 4.2')
20
+ s.add_dependency('activerecord', '>= 3.2', '< 6.1')
22
21
 
23
- s.add_development_dependency('rake', '~> 10.4.2')
24
- s.add_development_dependency('rspec-rails', '~> 2.14.0')
25
- s.add_development_dependency('mysql2', '~> 0.3.10')
22
+ s.add_development_dependency('rake', '~> 12.3.3')
23
+ s.add_development_dependency('rspec', '~> 3.9.0')
26
24
  end
@@ -0,0 +1,149 @@
1
+ require 'spec_helper'
2
+
3
+ describe Polymorpheus::Interface::BelongsToPolymorphic do
4
+ let(:hero) { Hero.create! }
5
+ let(:villain) { Villain.create! }
6
+ let(:superhero) { Superhero.create! }
7
+ let(:alien_demigod) { AlienDemigod.create! }
8
+
9
+ before do
10
+ create_table :story_arcs do |t|
11
+ t.references :hero
12
+ t.references :villain
13
+ end
14
+ create_table :heros
15
+ create_table :villains
16
+ end
17
+
18
+ specify do
19
+ expect(StoryArc::POLYMORPHEUS_ASSOCIATIONS).to eq(%w[hero villain])
20
+ end
21
+ specify do
22
+ expect(Superpower::POLYMORPHEUS_ASSOCIATIONS).to eq(%w[superhero supervillain])
23
+ end
24
+
25
+ describe "setter methods for ActiveRecord objects" do
26
+ let(:story_arc) { StoryArc.new(attributes) }
27
+ let(:attributes) { {} }
28
+
29
+ it "sets the correct attribute value for the setter" do
30
+ story_arc.character = hero
31
+ expect(story_arc.hero_id).to eq(hero.id)
32
+ expect(story_arc.villain_id).to eq(nil)
33
+ end
34
+
35
+ it "sets competing associations to nil" do
36
+ story_arc.character = hero
37
+ expect(story_arc.hero_id).to eq(hero.id)
38
+ story_arc.character = villain
39
+ expect(story_arc.villain_id).to eq(villain.id)
40
+ expect(story_arc.hero_id).to eq(nil)
41
+ end
42
+
43
+ it "throws an error if the assigned object isn't a valid type" do
44
+ create_table :trees
45
+
46
+ tree = Tree.create!
47
+ expect { story_arc.character = tree }.to raise_error(
48
+ Polymorpheus::Interface::InvalidTypeError,
49
+ "Invalid type. Must be one of {hero, villain}"
50
+ )
51
+ end
52
+
53
+ it "does not throw an error if the assigned object is a subclass of a
54
+ valid type" do
55
+ expect { story_arc.character = superhero }.not_to raise_error
56
+ expect(story_arc.hero_id).to eq(superhero.id)
57
+ end
58
+
59
+ it "does not throw an error if the assigned object is a descendant of a
60
+ valid type" do
61
+ expect { story_arc.character = alien_demigod }.not_to raise_error
62
+ expect(story_arc.hero_id).to eq(alien_demigod.id)
63
+ end
64
+ end
65
+
66
+ describe "setter methods for objects inheriting from ActiveRecord objects" do
67
+ let(:superpower) { Superpower.new }
68
+
69
+ before do
70
+ create_table :superpowers do |t|
71
+ t.references :superhero
72
+ t.references :supervillain
73
+ end
74
+ end
75
+
76
+ it "throws an error if the assigned object is an instance of the parent
77
+ ActiveRecord class" do
78
+ expect { superpower.wielder = hero }.to raise_error(
79
+ Polymorpheus::Interface::InvalidTypeError,
80
+ "Invalid type. Must be one of {superhero, supervillain}"
81
+ )
82
+ end
83
+
84
+ it "works if the assigned object is of the specified class" do
85
+ expect { superpower.wielder = superhero }.not_to raise_error
86
+ expect(superpower.superhero_id).to eq(superhero.id)
87
+ end
88
+
89
+ it "works if the assigned object is an instance of a child class" do
90
+ expect { superpower.wielder = alien_demigod }.not_to raise_error
91
+ expect(superpower.superhero_id).to eq(alien_demigod.id)
92
+ end
93
+ end
94
+
95
+ describe '#polymorpheus exposed interface method' do
96
+ subject(:interface) { story_arc.polymorpheus }
97
+
98
+ context 'when there is no relationship defined' do
99
+ let(:story_arc) { StoryArc.new }
100
+
101
+ specify do
102
+ expect(interface.associations).to match_associations(:hero, :villain)
103
+ end
104
+ specify { expect(interface.active_association).to eq nil }
105
+ specify { expect(interface.query_condition).to eq nil }
106
+ end
107
+
108
+ context 'when there is are multiple relationships defined' do
109
+ let(:story_arc) { StoryArc.new(hero_id: hero.id, villain_id: villain.id) }
110
+
111
+ specify do
112
+ expect(interface.associations).to match_associations(:hero, :villain)
113
+ end
114
+ specify { expect(interface.active_association).to eq nil }
115
+ specify { expect(interface.query_condition).to eq nil }
116
+ end
117
+
118
+ context 'when there is one relationship defined through the id value' do
119
+ let(:story_arc) { StoryArc.new(hero_id: hero.id) }
120
+
121
+ specify do
122
+ expect(interface.associations).to match_associations(:hero, :villain)
123
+ end
124
+ specify { expect(interface.active_association).to be_association(:hero) }
125
+ specify { expect(interface.query_condition).to eq('hero_id' => hero.id) }
126
+ end
127
+
128
+ context 'when there is one relationship defined through the setter' do
129
+ let(:story_arc) { StoryArc.new(character: hero) }
130
+
131
+ specify do
132
+ expect(interface.associations).to match_associations(:hero, :villain)
133
+ end
134
+ specify { expect(interface.active_association).to be_association(:hero) }
135
+ specify { expect(interface.query_condition).to eq('hero_id' => hero.id) }
136
+ end
137
+
138
+ context 'when there is one association, to a new record' do
139
+ let(:new_hero) { Hero.new }
140
+ let(:story_arc) { StoryArc.new(character: new_hero) }
141
+
142
+ specify do
143
+ expect(interface.associations).to match_associations(:hero, :villain)
144
+ end
145
+ specify { expect(interface.active_association).to be_association(:hero) }
146
+ specify { expect(interface.query_condition).to eq nil }
147
+ end
148
+ end
149
+ end