shiftable 0.4.1 → 0.5.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
2
  SHA256:
3
- metadata.gz: 7792e05d55078e47ad4fec714ebc5aeed51c6373ba64d31e410ab8cdfe01eb44
4
- data.tar.gz: d1435eb2ab87f9e81367d73036d8bf23c270eccb29157cc1eaf364da9ef8fcb2
3
+ metadata.gz: 678fdd1fc65199227c201ecd4346e4c0019c78747d949ac9b4dcba8b38158c24
4
+ data.tar.gz: 3f527ed0b2e70a2e677b006320b3e667d0512ccfcacc5bb76232129b9f37c6d0
5
5
  SHA512:
6
- metadata.gz: 8fc584eefc4ab6e4bbdcbb22a3b856f8baa2dff98daf5eceb149ced8865fa5c1dc020bcf1cde2918df47f3b0545c9c1827e9f369e2131c4bb5498804c1cce37c
7
- data.tar.gz: d7970f2f74a80bdd5a657e89846a5eddd54d7396cd000d3022b0ba1782336b464586ea1391a23af6ff8df6b7d6047715c571cdfb43d55145744504f5ced7e4db
6
+ metadata.gz: c281d086eabfbce50dcd9f3098374656a8c7536b0ce1d5e21d5262c6e205172f90930aa7120a747904d6599313c235adba0bc5487011fa7b5df0e8c90af9157b
7
+ data.tar.gz: 217eb1fb303dbc45ccb962bddbff8c4dc246b20351b1f1f2312b9e4ea8c4db7a90360c089b040f0276e544a38193d01feb1728bd6f3949dfe849509a12d38a64
data/CHANGELOG.md CHANGED
@@ -1,7 +1,28 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.4.0] - 2021-10-27
3
+ ### Added
4
+
5
+
6
+ ### Changed
7
+
8
+
9
+ ### Fixed
10
+
11
+
12
+ ### Removed
13
+
4
14
 
15
+ ## [0.5.0] - 2021-11-12
16
+ ### Added
17
+
18
+ - Support for Polymorphic associations (see examples in specs or README)
19
+
20
+ ## [0.4.1] - 2021-11-10
21
+ ### Fixed
22
+
23
+ - Documentation typos in README
24
+
25
+ ## [0.4.0] - 2021-10-27
5
26
  ### Changed
6
27
 
7
28
  - option :preflight_checks renamed to :precheck
@@ -11,7 +32,6 @@
11
32
  - Even more 100% spec coverage
12
33
 
13
34
  ## [0.3.0] - 2021-10-26
14
-
15
35
  ### Changed
16
36
 
17
37
  - Internal rewrite to improve maintainability
@@ -22,7 +42,6 @@
22
42
  - Even more 100% spec coverage
23
43
 
24
44
  ## [0.2.0] - 2021-10-24
25
-
26
45
  ### Changed
27
46
 
28
47
  - option `before_save` is now `before_shift` as originally documented
@@ -34,7 +53,6 @@
34
53
  - Documentation
35
54
 
36
55
  ## [0.1.1] - 2021-10-23
37
-
38
56
  ### Fixed
39
57
 
40
58
  - Github Actions build
@@ -44,7 +62,6 @@
44
62
  - Linting
45
63
 
46
64
  ## [0.1.0] - 2021-10-23
47
-
48
65
  ### Added
49
66
 
50
67
  - Initial release
data/CODE_OF_CONDUCT.md CHANGED
File without changes
data/CONTRIBUTING.md CHANGED
File without changes
data/LICENSE.txt CHANGED
File without changes
data/README.md CHANGED
@@ -104,7 +104,7 @@ end
104
104
 
105
105
  This works as you would expect with STI (single table inheritance) classes, i.e. when defined on a subclass, only the records of that class get shifted.
106
106
 
107
- ### Multiple association on a single class
107
+ ### Multiple associations on a single class
108
108
 
109
109
  What if the captain and the spaceship have a boss... the space
110
110
  federation! And in a run-in with their arch-Nemesis the Plinth-inth,
@@ -114,7 +114,7 @@ all federation spaceships are commandeered! You are ruined!
114
114
 
115
115
  class Spaceship < ActiveRecord::Base
116
116
  belongs_to :space_federation
117
- extend Shiftable::Collection.new belongs_to: :space_federation, has_one: :spaceship,
117
+ extend Shiftable::Collection.new belongs_to: :space_federation, has_many: :spaceships,
118
118
  before_shift: lambda { |shifting_rel:, shift_to:, shift_from:|
119
119
  shifting_rel.each { |spaceship| spaceship.federation_changes += 1 }
120
120
  }
@@ -129,6 +129,68 @@ class SpaceFederation < ActiveRecord::Base
129
129
  end
130
130
  ```
131
131
 
132
+ ### Polymorphism and has_many through
133
+
134
+ ```ruby
135
+ class SpaceTreaty < ActiveRecord::Base
136
+ has_many :space_treaty_signature
137
+ end
138
+
139
+ class SpaceTreatySignature < ActiveRecord::Base
140
+ belongs_to :space_treaty
141
+ belongs_to :signatory, polymorphic: true
142
+ # When two space federations assimilate (i.e. merge) to form a single larger federation,
143
+ # they become party to (i.e. signatories of) all the treaties that had been signed by either.
144
+ # In practical terms, this means:
145
+ #
146
+ # surviving_federation = SpaceFederation.find(1)
147
+ # assimilated_federation = SpaceFederation.find(2)
148
+ # SpaceTreatySignature.where(
149
+ # signatory_id: assimilated_federation_id,
150
+ # signatory_type: "SpaceFederation"
151
+ # ).update_all(
152
+ # signatory_id: surviving_federation.id
153
+ # )
154
+ extend Shiftable::Collection.new(
155
+ belongs_to: :signatory, has_many: :space_treaty_signature,
156
+ polymorphic_type: "SpaceFederation",
157
+ method_prefix: "space_federation_",
158
+ before_shift: lambda { |shifting_rel:, shift_to:, shift_from:|
159
+ # Each item in shifting_rel is an instance of the class where Shiftable::Collection is defined,
160
+ # in this case: SpaceTreatySignature
161
+ # And each of them has a signatory which is of type "SpaceFederation",
162
+ # because a polymorphic collection only targets one type.
163
+ # shifting_rel.each { |signature| signature.signatory == "SpaceFederation" }
164
+ }
165
+ )
166
+ end
167
+
168
+ class SpaceFederation < ActiveRecord::Base
169
+ has_many :space_treaty_signature, as: :signatory
170
+ has_many :space_treaties, through: :space_treaty_signatures, as: :signatory
171
+ has_many :treaty_planets, class_name: "Planet", through: :space_treaty_signatures, as: :signatory
172
+ has_many :treaty_stations, class_name: "SpaceStation", through: :space_treaty_signatures, as: :signatory
173
+ def assimilate_from(other_federation)
174
+ SpaceTreatySignature.space_federation_shift_cx(shift_to: self, shift_from: other_federation)
175
+ end
176
+ end
177
+
178
+ # Including Planet and SpaceStation, for completeness of the example as the other "types" of polymorphic signatories
179
+ class Planet < ActiveRecord::Base
180
+ has_many :space_treaty_signature, as: :signatory
181
+ has_many :space_treaties, through: :space_treaty_signatures
182
+ has_many :treaty_federations, class_name: "SpaceFederation", through: :space_treaty_signatures, as: :signatory
183
+ has_many :treaty_stations, class_name: "SpaceStation", through: :space_treaty_signatures, as: :signatory
184
+ end
185
+
186
+ class SpaceStation < ActiveRecord::Base
187
+ has_many :space_treaty_signature, as: :signatory
188
+ has_many :space_treaties, through: :space_treaty_signatures
189
+ has_many :treaty_federations, class_name: "SpaceFederation", through: :space_treaty_signatures, as: :signatory
190
+ has_many :treaty_planets, class_name: "Planet", through: :space_treaty_signatures, as: :signatory
191
+ end
192
+ ```#<--rubocop/md-->#<--rubocop/md-->`
193
+
132
194
  ### Complete example
133
195
 
134
196
  Putting it all together...
@@ -148,7 +210,7 @@ class Spaceship < ActiveRecord::Base
148
210
  before_shift: ->(shifting:, shift_to:, shift_from:) { shifting.ownership_changes += 1 }
149
211
 
150
212
  belongs_to :space_federation
151
- extend Shiftable::Collection.new belongs_to: :space_federation, has_one: :spaceship,
213
+ extend Shiftable::Collection.new belongs_to: :space_federation, has_many: :spaceships,
152
214
  before_shift: lambda { |shifting_rel:, shift_to:, shift_from:|
153
215
  shifting_rel.each { |spaceship| spaceship.federation_changes += 1 }
154
216
  }
@@ -157,11 +219,47 @@ end
157
219
  class SpaceFederation < ActiveRecord::Base
158
220
  has_many :captains
159
221
  has_many :spaceships
222
+ has_many :space_treaty_signature, as: :signatory
223
+ has_many :space_treaties, through: :space_treaty_signatures, as: :signatory
224
+ has_many :treaty_planets, class_name: "Planet", through: :space_treaty_signatures, as: :signatory
225
+ has_many :treaty_stations, class_name: "SpaceStation", through: :space_treaty_signatures, as: :signatory
226
+
227
+ def assimilate_from(other_federation)
228
+ SpaceTreatySignature.space_federation_shift_cx(shift_to: self, shift_from: other_federation)
229
+ end
160
230
 
161
231
  def all_spaceships_commandeered_by(nemesis_federation)
162
232
  Spaceship.shift_cx(shift_to: nemesis_federation, shift_from: self)
163
233
  end
164
234
  end
235
+ class SpaceTreaty < ActiveRecord::Base
236
+ has_many :space_treaty_signatures
237
+ end
238
+
239
+ class SpaceTreatySignature < ActiveRecord::Base
240
+ belongs_to :space_treaty
241
+ belongs_to :signatory, polymorphic: true
242
+ extend Shiftable::Collection.new(
243
+ belongs_to: :signatory, has_many: :space_treaty_signatures,
244
+ polymorphic_type: "SpaceFederation",
245
+ method_prefix: "space_federation_"
246
+ )
247
+ end
248
+
249
+ # Including Planet and SpaceStation, for completeness of the example as the other "types" of polymorphic signatories
250
+ class Planet < ActiveRecord::Base
251
+ has_many :space_treaty_signatures, as: :signatory
252
+ has_many :space_treaties, through: :space_treaty_signatures
253
+ has_many :treaty_federations, class_name: "SpaceFederation", through: :space_treaty_signatures, as: :signatory
254
+ has_many :treaty_stations, class_name: "SpaceStation", through: :space_treaty_signatures, as: :signatory
255
+ end
256
+
257
+ class SpaceStation < ActiveRecord::Base
258
+ has_many :space_treaty_signature, as: :signatory
259
+ has_many :space_treaties, through: :space_treaty_signatures
260
+ has_many :treaty_federations, class_name: "SpaceFederation", through: :space_treaty_signatures, as: :signatory
261
+ has_many :treaty_planets, class_name: "Planet", through: :space_treaty_signatures, as: :signatory
262
+ end
165
263
  ```
166
264
 
167
265
  ... stay tuned!
@@ -16,7 +16,7 @@ module Shiftable
16
16
  class Collection < Module
17
17
  # associations: belongs_to, has_many
18
18
  # options: method_prefix, before_shift
19
- def initialize(belongs_to:, has_many:, method_prefix: nil, before_shift: nil)
19
+ def initialize(belongs_to:, has_many:, polymorphic: nil, method_prefix: nil, before_shift: nil)
20
20
  # Ruby's Module initializer doesn't take any arguments
21
21
  super()
22
22
 
@@ -35,12 +35,13 @@ module Shiftable
35
35
  has_many: has_many.to_s.to_sym
36
36
  },
37
37
  options: {
38
+ polymorphic: polymorphic,
38
39
  method_prefix: method_prefix,
39
40
  # will prevent the save if it returns false
40
41
  # allows for any custom logic to be run, such as setting attributes, prior to the shift (save).
41
42
  before_shift: before_shift
42
43
  },
43
- type: :cx
44
+ type: polymorphic ? :pcx : :cx
44
45
  )
45
46
  end
46
47
 
@@ -62,11 +63,12 @@ module Shiftable
62
63
  module ShiftCollectionModulizer
63
64
  def to_mod(signature)
64
65
  prefix = signature.method_prefix
66
+ type = signature.type
65
67
  Module.new do
66
- define_method(:"#{prefix}shift_cx_column") do
67
- signature.shift_column
68
+ define_method(:"#{prefix}shift_#{type}_column") do
69
+ signature.send("shift_#{type}_column")
68
70
  end
69
- define_method(:"#{prefix}shift_cx") do |shift_to:, shift_from:|
71
+ define_method(:"#{prefix}shift_#{type}") do |shift_to:, shift_from:|
70
72
  signature.shift_data!(shift_to: shift_to, shift_from: shift_from)
71
73
  end
72
74
  end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Shiftable
4
4
  class ModSignature
5
- VALID_TYPES = %i[sg cx].freeze
6
- VALID_ASSOCIATIONS = { sg: %i[belongs_to has_one], cx: %i[belongs_to has_many] }.freeze
5
+ VALID_ASSOCIATIONS = { sg: %i[belongs_to has_one], cx: %i[belongs_to has_many], pcx: %i[belongs_to has_many] }.freeze
6
+ VALID_TYPES = VALID_ASSOCIATIONS.keys.dup.freeze
7
7
  DEFAULT_BEFORE_SHIFT = ->(*_) { true }
8
8
  attr_reader :associations, :options, :type, :base
9
9
 
@@ -24,6 +24,14 @@ module Shiftable
24
24
  raise ArgumentError, "exactly two distinct associations must be provided" if invalid_number_of_associations?
25
25
  end
26
26
 
27
+ def polymorphic_type
28
+ options.dig(:polymorphic, :type)
29
+ end
30
+
31
+ def polymorphic_as
32
+ options.dig(:polymorphic, :as)
33
+ end
34
+
27
35
  def invalid_type?
28
36
  !VALID_TYPES.include?(type)
29
37
  end
@@ -48,21 +56,21 @@ module Shiftable
48
56
  end
49
57
 
50
58
  def validate_relationships
51
- bt = base.reflect_on_association(belongs_to)
52
- raise ArgumentError, "Unable to find belongs_to: :#{belongs_to} in #{base}" unless bt
53
-
54
- klass = bt.klass
55
- hr = klass.reflect_on_association(has_rel)
56
- raise ArgumentError, "Unable to find #{has_rel_name}: :#{has_rel} in #{klass}" unless hr
59
+ bt_reflection = base.reflect_on_association(belongs_to)
60
+ raise ArgumentError, "Unable to find belongs_to: :#{belongs_to} in #{base}" unless bt_reflection
61
+ # We can't validate any further if the reflection is polymorphic
62
+ return true if bt_reflection.polymorphic?
63
+
64
+ klass = bt_reflection.klass
65
+ has_reflection = klass.reflect_on_association(has_rel)
66
+ raise ArgumentError, "Unable to find #{has_rel_name}: :#{has_rel} in #{klass}" unless has_reflection
57
67
  end
58
68
 
59
69
  module CxMethods
60
- def has_many
70
+ def has_rel
61
71
  associations[:has_many]
62
72
  end
63
73
 
64
- alias has_rel has_many
65
-
66
74
  def shift_data!(shift_to:, shift_from:)
67
75
  validate_relationships
68
76
  shifting_rel = ShiftingRelation.new(
@@ -77,13 +85,35 @@ module Shiftable
77
85
  end
78
86
  end
79
87
 
88
+ module PcxMethods
89
+ # This method could be defined for parity, but it is never used.
90
+ # def has_rel
91
+ # associations[:has_many]
92
+ # end
93
+
94
+ def shift_data!(shift_to:, shift_from:)
95
+ validate_relationships
96
+ shifting_rel = ShiftingPolymorphicRelation.new(
97
+ to: shift_to,
98
+ from: shift_from,
99
+ column: {
100
+ type: polymorphic_type,
101
+ as: polymorphic_as,
102
+ id_column: shift_pcx_column
103
+ },
104
+ base: base
105
+ )
106
+ shifting_rel.shift do |result|
107
+ before_shift&.call(shifting_rel: result, shift_to: shift_to, shift_from: shift_from)
108
+ end
109
+ end
110
+ end
111
+
80
112
  module SgMethods
81
- def has_one
113
+ def has_rel
82
114
  associations[:has_one]
83
115
  end
84
116
 
85
- alias has_rel has_one
86
-
87
117
  # Do not move record if a record already exists (we are shifting a "has_one" association, after all)
88
118
  def precheck
89
119
  options[:precheck]
@@ -97,7 +127,7 @@ module Shiftable
97
127
  column: shift_column,
98
128
  base: base
99
129
  ) do
100
- !precheck || !shift_to.send(has_one)
130
+ !precheck || !shift_to.send(has_rel)
101
131
  end
102
132
  shifting.shift do |result|
103
133
  before_shift&.call(shifting: result, shift_to: shift_to, shift_from: shift_from)
@@ -122,9 +152,16 @@ module Shiftable
122
152
  options[:before_shift] || DEFAULT_BEFORE_SHIFT
123
153
  end
124
154
 
155
+ def shift_pcx_column
156
+ "#{polymorphic_as}_id"
157
+ end
158
+
125
159
  def shift_column
126
160
  reflection = base.reflect_on_association(belongs_to).klass.reflect_on_association(has_rel)
127
161
  reflection.foreign_key
128
162
  end
163
+
164
+ alias shift_sg_column shift_column
165
+ alias shift_cx_column shift_column
129
166
  end
130
167
  end
File without changes
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shiftable
4
+ # Gets data to be shifted
5
+ class ShiftingPolymorphicRelation < Shifting
6
+ include Enumerable
7
+
8
+ def polymorphic_id_column
9
+ column[:id_column]
10
+ end
11
+
12
+ def polymorphic_type_column
13
+ "#{column[:as]}_type"
14
+ end
15
+
16
+ def found?
17
+ result.any?
18
+ end
19
+
20
+ def each(&block)
21
+ result.each(&block)
22
+ end
23
+
24
+ # @return result (once it is shifted)
25
+ def shift
26
+ return false unless found?
27
+
28
+ each do |record|
29
+ record.send("#{polymorphic_id_column}=", to.id)
30
+ end
31
+ @run_save = yield result if block_given?
32
+ each(&:save) if run_save
33
+ result
34
+ end
35
+
36
+ private
37
+
38
+ def query
39
+ base.where(
40
+ polymorphic_type_column => column[:type],
41
+ polymorphic_id_column => from.id
42
+ )
43
+ end
44
+ end
45
+ end
File without changes
File without changes
@@ -70,7 +70,7 @@ module Shiftable
70
70
  prefix = signature.method_prefix
71
71
  Module.new do
72
72
  define_method(:"#{prefix}shift_column") do
73
- signature.shift_column
73
+ signature.send("shift_#{signature.type}_column")
74
74
  end
75
75
  define_method(:"#{prefix}shift_single") do |shift_to:, shift_from:|
76
76
  signature.shift_data!(shift_to: shift_to, shift_from: shift_from)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shiftable
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/shiftable.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative "shiftable/version"
4
4
  require_relative "shiftable/shifting"
5
5
  require_relative "shiftable/shifting_record"
6
+ require_relative "shiftable/shifting_polymorphic_relation"
6
7
  require_relative "shiftable/shifting_relation"
7
8
  require_relative "shiftable/mod_signature"
8
9
  require_relative "shiftable/collection"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shiftable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Boling
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-11-10 00:00:00.000000000 Z
11
+ date: 2021-11-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -181,6 +181,7 @@ files:
181
181
  - lib/shiftable/collection.rb
182
182
  - lib/shiftable/mod_signature.rb
183
183
  - lib/shiftable/shifting.rb
184
+ - lib/shiftable/shifting_polymorphic_relation.rb
184
185
  - lib/shiftable/shifting_record.rb
185
186
  - lib/shiftable/shifting_relation.rb
186
187
  - lib/shiftable/single.rb