polymorpheus 2.2.0 → 3.3.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.
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