shiftable 0.1.0 → 0.4.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: 2aee0ddcd18b646324ef8ec890c7fc50aaf2af9c6b630c5267d3ec0143a7dedb
4
- data.tar.gz: c6bd0a6a3ddc61517dc9e162c4cf9e9861e23f832baaf8e013c3bbb12e97d868
3
+ metadata.gz: 62926c718edd257728210a0870ea5eca44aeb35b33a929d87d2efb8a5f902910
4
+ data.tar.gz: b4c35fcc2a3eff45d992b5260e6d4f2f13d870782f40d06285172eb2f1207f9d
5
5
  SHA512:
6
- metadata.gz: 6e6b917230a28123480e5032766ee0e26dfc44c887f177d59b6f44fc9c1659eec0695a1513415f126a3de168b4315498eb7b26905e07350c16d72cec6e17234e
7
- data.tar.gz: 0f4765f281ec4cf5bd97e874fe6ee63dc171df5f178d028d99a1c89d886e00ea5906931e1f192467ddcd1206c2d473c8a6d738db0a0db3f8b9bd10454f171da1
6
+ metadata.gz: 8c21a1f0c51b4a3cb715730fe07bb29d562a84d8dd79b558a3ca06c6a7bf20032928225b41aa1e749b4a7a85afe4fdb33cbf9d068b7539ef622665272f1f8115
7
+ data.tar.gz: d9bc2d54fe6fb3f15f781b33ddf83c6abfa91f7b66e426274bc9ef07595e61a9c6b4f42a2ffe27e6b8cce5453dbcc0221ac07261bddb762a3a9eba3e79d7cef1
data/CHANGELOG.md CHANGED
@@ -1,10 +1,62 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2021-10-27
4
+
5
+ ### Changed
6
+
7
+ - option :preflight_checks renamed to :precheck
8
+
9
+ ### Added
10
+
11
+ - Even more 100% spec coverage
12
+
13
+ ## [0.3.0] - 2021-10-26
14
+
15
+ ### Changed
16
+
17
+ - Internal rewrite to improve maintainability
18
+ - Extreme edge cases involving incorrect configuration will raise slightly different errors.
19
+
20
+ ### Added
21
+
22
+ - Even more 100% spec coverage
23
+
24
+ ## [0.2.0] - 2021-10-24
25
+
26
+ ### Changed
27
+
28
+ - option `before_save` is now `before_shift` as originally documented
29
+
30
+ ### Updated
31
+
32
+ - Github Actions now test all supported Rubies
33
+ - Linting
34
+ - Documentation
35
+
36
+ ## [0.1.1] - 2021-10-23
37
+
38
+ ### Fixed
39
+
40
+ - Github Actions build
41
+
42
+ ### Updated
43
+
44
+ - Linting
45
+
3
46
  ## [0.1.0] - 2021-10-23
4
47
 
5
48
  ### Added
6
49
 
7
50
  - Initial release
8
- - feat: supports shifting of records associated as has_one / belongs_to
51
+ - feat: supports shifting of records associated as has_one / belongs_to and has_many / belongs_to, including with STI.
52
+ - 100% test coverage
53
+
54
+ [0.1.0]: https://github.com/pboling/shiftable/releases/tag/v0.1.0
55
+
56
+ [0.1.1]: https://github.com/pboling/shiftable/releases/tag/v0.1.1
57
+
58
+ [0.2.0]: https://github.com/pboling/shiftable/releases/tag/v0.2.0
59
+
60
+ [0.3.0]: https://github.com/pboling/shiftable/releases/tag/v0.3.0
9
61
 
10
- [0.1.0]: https://github.com/pboling/shiftable/releases/tag/v0.1.0
62
+ [0.4.0]: https://github.com/pboling/shiftable/releases/tag/v0.4.0
data/README.md CHANGED
@@ -5,12 +5,38 @@ Do your Spaceships belong to Captains, but sometimes a Captain will retire, and
5
5
  We've all been there. This gem provides structure around the process of "shifting" your records from one associated
6
6
  record to a new record.
7
7
 
8
+ | Project | Shiftable |
9
+ |------------------------ | ----------------------- |
10
+ | gem name | [shiftable](https://rubygems.org/gems/shiftable) |
11
+ | license | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) |
12
+ | download rank | [![Downloads Today](https://img.shields.io/gem/rd/shiftable.svg)](https://github.com/pboling/shiftable) |
13
+ | version | [![Version](https://img.shields.io/gem/v/shiftable.svg)](https://rubygems.org/gems/shiftable) |
14
+ | dependencies | [![Depfu](https://badges.depfu.com/badges/0412727b7e3b740b950a683eebc708e2/count.svg)](https://depfu.com/github/pboling/shiftable?project_id=32594) |
15
+ | unit tests | [![unit tests](https://github.com/pboling/shiftable/actions/workflows/test.yml/badge.svg)](https://github.com/pboling/shiftable/actions) |
16
+ | lint status | [![lint status](https://github.com/pboling/shiftable/actions/workflows/style.yml/badge.svg)](https://github.com/pboling/shiftable/actions) |
17
+ | unsupported status | [![unsupported status](https://github.com/pboling/shiftable/actions/workflows/unsupported.yml/badge.svg)](https://github.com/pboling/shiftable/actions) |
18
+ | test coverage | [![Test Coverage](https://api.codeclimate.com/v1/badges/a53aa8b7c413b950d519/test_coverage)](https://codeclimate.com/github/pboling/shiftable/test_coverage) |
19
+ | maintainability | [![Maintainability](https://api.codeclimate.com/v1/badges/a53aa8b7c413b950d519/maintainability)](https://codeclimate.com/github/pboling/shiftable/maintainability) |
20
+ | code triage | [![Open Source Helpers](https://www.codetriage.com/pboling/shiftable/badges/users.svg)](https://www.codetriage.com/pboling/shiftable) |
21
+ | homepage | [on Github.com][homepage], [on Railsbling.com][blogpage] |
22
+ | documentation | [on RDoc.info][documentation] |
23
+ | Spread ~♡ⓛⓞⓥⓔ♡~ | [🌏][aboutme], [👼][angelme], [💻][coderme], [![Tweet @ Peter][followme-img]][tweetme], [🌹][politicme] |
24
+
25
+ ## Compatibility
26
+
27
+ Targeted ruby compatibility is non-EOL versions of Ruby, currently 2.6, 2.7, and 3.0, but may work on older Rubies back
28
+ to 2.0, though it is limited to 2.5 in the gemspec. Feel free to fork if you need something older! Targeted ActiveRecord
29
+ (Rails not required) compatibility follows the same scheme
30
+ as [Rails Security Issue maintenance policy](https://guides.rubyonrails.org/maintenance_policy.html#security-issues),
31
+ currently 6.1, 6.0, 5.2, but it is highly likely that this code will work in any version of ActiveRecord/Rails that runs
32
+ on Ruby 2+.
33
+
8
34
  ## Installation
9
35
 
10
36
  Add this line to your application's Gemfile:
11
37
 
12
38
  ```ruby
13
- gem 'shiftable'
39
+ gem "shiftable"
14
40
  ```
15
41
 
16
42
  And then execute:
@@ -61,32 +87,86 @@ But how can you accomplish this? If you used the `shiftable` gem, won't take but
61
87
 
62
88
  class Spaceship < ActiveRecord::Base
63
89
  belongs_to :captain
64
- extend Shiftable::Single.new(
65
- belongs_to: :captain,
66
- has_one: :spaceship,
67
- preflight_checks: true,
68
- before_shift: ->(shifting:, shift_to:, shift_from:) { shifting.ownership_changes += 1 }
69
- )
90
+ extend Shiftable::Single.new belongs_to: :captain, has_one: :spaceship, precheck: true,
91
+ before_shift: ->(shifting:, shift_to:, shift_from:) { shifting.ownership_changes += 1 }
92
+ end
93
+ ```
94
+
95
+ NOTE: It doesn't matter if the extend occurs before or after the association macro `belongs_to`. In fact, it doesn't matter so much that you can even do this...
96
+
97
+ ```ruby
98
+
99
+ class Spaceship < ActiveRecord::Base
100
+ belongs_to :captain
101
+
102
+ class << self
103
+ include Shiftable::Single.new belongs_to: :captain, has_one: :spaceship, precheck: true,
104
+ before_shift: lambda { |shifting:, shift_to:, shift_from:|
105
+ shifting.ownership_changes += 1
106
+ }
107
+ end
70
108
  end
71
109
  ```
72
110
 
111
+ ### Single Table Inheritance
112
+
113
+ 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.
114
+
73
115
  ### Multiple association on a single class
74
116
 
75
- What if the Captain is really all that though? What if the captain and the spaceship have a boss... the space
76
- federation!
117
+ What if the captain and the spaceship have a boss... the space
118
+ federation! And in a run-in with their arch-Nemesis the Plinth-inth,
119
+ all federation spaceships are commandeered! You are ruined!
77
120
 
78
121
  ```ruby
79
122
 
123
+ class Spaceship < ActiveRecord::Base
124
+ belongs_to :space_federation
125
+ extend Shiftable::Collection.new belongs_to: :space_federation, has_one: :spaceship,
126
+ before_shift: lambda do |shifting_rel:, shift_to:, shift_from:|
127
+ shifting_rel.each { |spaceship| spaceship.federation_changes += 1 }
128
+ end
129
+ end
130
+
131
+ class SpaceFederation < ActiveRecord::Base
132
+ has_many :spaceships
133
+
134
+ def all_spaceships_commandeered_by(nemesis_federation)
135
+ Spaceship.shift_cx(shift_to: nemesis_federation, shift_from: self)
136
+ end
137
+ end
138
+ ```
139
+
140
+ ### Complete example
141
+
142
+ Putting it all together...
143
+
144
+ ```ruby
145
+ class Captain < ActiveRecord::Base
146
+ has_one :spaceship
147
+
148
+ def sell_spaceship_to(nemesis_captain)
149
+ Spaceship.shift_single(shift_to: nemesis_captain, shift_from: self)
150
+ end
151
+ end
152
+
80
153
  class Spaceship < ActiveRecord::Base
81
154
  belongs_to :captain
155
+ extend Shiftable::Single.new belongs_to: :captain, has_one: :spaceship, precheck: true,
156
+ before_shift: ->(shifting:, shift_to:, shift_from:) { shifting.ownership_changes += 1 }
157
+
82
158
  belongs_to :space_federation
159
+ extend Shiftable::Collection.new belongs_to: :space_federation, has_one: :spaceship,
160
+ before_shift: lambda do |shifting_rel:, shift_to:, shift_from:|
161
+ shifting_rel.each { |spaceship| spaceship.federation_changes += 1 }
162
+ end
83
163
  end
84
164
 
85
165
  class SpaceFederation < ActiveRecord::Base
86
166
  has_many :captains
87
167
  has_many :spaceships
88
168
 
89
- def sell_spaceship_to(nemesis_federation)
169
+ def all_spaceships_commandeered_by(nemesis_federation)
90
170
  Spaceship.shift_cx(shift_to: nemesis_federation, shift_from: self)
91
171
  end
92
172
  end
@@ -105,12 +185,63 @@ push git commits and the created tag, and push the `.gem` file to [rubygems.org]
105
185
 
106
186
  ## Contributing
107
187
 
108
- Bug reports and pull requests are welcome on GitHub at https://github.com/pboling/shiftable. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/pboling/shiftable/blob/master/CODE_OF_CONDUCT.md).
188
+ Bug reports and pull requests are welcome on GitHub at https://github.com/pboling/shiftable. This project is intended to
189
+ be a safe, welcoming space for collaboration, and contributors are expected to adhere to
190
+ the [code of conduct](https://github.com/pboling/shiftable/blob/master/CODE_OF_CONDUCT.md).
191
+
192
+ ## Code of Conduct
193
+
194
+ Everyone interacting in the Shiftable project's codebases, issue trackers, chat rooms and mailing lists is expected to
195
+ follow the [code of conduct](https://github.com/pboling/shiftable/blob/master/CODE_OF_CONDUCT.md).
196
+
197
+ ## Versioning
198
+
199
+ This library aims to adhere to [Semantic Versioning 2.0.0][semver]. Violations of this scheme should be reported as
200
+ bugs. Specifically, if a minor or patch version is released that breaks backward compatibility, a new version should be
201
+ immediately released that restores compatibility. Breaking changes to the public API will only be introduced with new
202
+ major versions.
203
+
204
+ As a result of this policy, you can (and should) specify a dependency on this gem using
205
+ the [Pessimistic Version Constraint][pvc] with two digits of precision.
206
+
207
+ For example:
208
+
209
+ ```ruby
210
+ spec.add_dependency "shiftable", "~> 0.2"
211
+ ```
109
212
 
110
213
  ## License
111
214
 
112
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
215
+ The gem is available as open source under the terms of
216
+ the [MIT License](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
217
+ .
113
218
 
114
- ## Code of Conduct
219
+ * Copyright (c) 2021 [Peter H. Boling][peterboling] of [Rails Bling][railsbling]
220
+
221
+ [license]: LICENSE
222
+
223
+ [semver]: http://semver.org/
224
+
225
+ [pvc]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint
226
+
227
+ [railsbling]: http://www.railsbling.com
228
+
229
+ [peterboling]: http://www.peterboling.com
230
+
231
+ [aboutme]: https://about.me/peter.boling
232
+
233
+ [angelme]: https://angel.co/peter-boling
234
+
235
+ [coderme]:http://coderwall.com/pboling
236
+
237
+ [followme-img]: https://img.shields.io/twitter/follow/galtzo.svg?style=social&label=Follow
238
+
239
+ [tweetme]: http://twitter.com/galtzo
240
+
241
+ [politicme]: https://nationalprogressiveparty.org
242
+
243
+ [documentation]: http://rdoc.info/github/pboling/shiftable/frames
244
+
245
+ [homepage]: https://github.com/pboling/shiftable/
115
246
 
116
- Everyone interacting in the Shiftable project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/pboling/shiftable/blob/master/CODE_OF_CONDUCT.md).
247
+ [blogpage]: http://www.railsbling.com/tags/shiftable/
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
4
3
  # Usage:
5
4
  #
6
5
  # class BlasterRounds < ActiveRecord::Base
@@ -15,74 +14,60 @@
15
14
  module Shiftable
16
15
  # Inheriting from Module is a powerful pattern. If you like it checkout the debug_logging gem!
17
16
  class Collection < Module
18
- def initialize(belongs_to:, has_many:, method_prefix: nil, preflight_checks: true, before_save: nil)
17
+ # associations: belongs_to, has_many
18
+ # options: method_prefix, before_shift
19
+ def initialize(belongs_to:, has_many:, method_prefix: nil, before_shift: nil)
20
+ # Ruby's Module initializer doesn't take any arguments
19
21
  super()
20
- raise ArgumentError, "belongs_to must be a symbol" unless belongs_to.is_a?(Symbol)
21
- raise ArgumentError, "has_many must be a symbol" unless has_many.is_a?(Symbol)
22
-
23
- # For the following, imagine you are a Spaceship Captain, the Spaceship belongs_to you, and it has only one Captain.
24
- # But you have to sell it to your nemesis!
25
- #
26
- # The name of the belongs_to association, defined on the shifting model, e.g. Spaceship
27
- # Normally a camel-cased, symbolized, version of the class name.
28
- # In the case where Spaceship belongs_to: :captain, this is :captain.
29
- @belongs_to = belongs_to
30
-
31
- # The name of the has_one association, defined on the shift_to/shift_from model, e.g. Captain.
32
- # Normally a camel-cased, symbolized, version of the class name.
33
- # In the case where Captain has_one: :spaceship, this is :spaceship.
34
- @has_many = has_many
35
22
 
36
- @method_prefix = method_prefix
37
-
38
- # will prevent the save if it returns false
39
- # allows for any custom logic to be run, such as setting shift_from attributes, prior to the shift is saved.
40
- @before_save = before_save
23
+ @signature = ModSignature.new(
24
+ # For the following, imagine you are a Space Federation, each Spaceship in the fleet belongs_to you,
25
+ # i.e. the federation has_many spaceships.
26
+ # But you lose the war, and your nemesis commandeers all your ships!
27
+ associations: {
28
+ # The name of the belongs_to association, defined on the shifting model, e.g. Spaceship
29
+ # Normally a camel-cased, symbolized, version of the class name.
30
+ # In the case where Spaceship belongs_to: :space_federation, this is :space_federation.
31
+ belongs_to: belongs_to.to_s.to_sym,
32
+ # The name of the has_many association, defined on the shift_to/shift_from model, e.g. SpaceFederation.
33
+ # Normally a camel-cased, symbolized, version of the class name.
34
+ # In the case where SpaceFederation has_many: :spaceships, this is :spaceships.
35
+ has_many: has_many.to_s.to_sym
36
+ },
37
+ options: {
38
+ method_prefix: method_prefix,
39
+ # will prevent the save if it returns false
40
+ # allows for any custom logic to be run, such as setting attributes, prior to the shift (save).
41
+ before_shift: before_shift
42
+ },
43
+ type: :cx
44
+ )
41
45
  end
42
46
 
47
+ # NOTE: Possible difference in how inheritance works when using extend vs include
48
+ # with Shiftable::Collection.new
43
49
  def extended(base)
44
- shift_cx_modulizer = ShiftCollectionModulizer.to_mod(@belongs_to, @has_many, @method_prefix,
45
- @before_save)
50
+ shift_cx_modulizer = ShiftCollectionModulizer.to_mod(@signature.add_base(base))
46
51
  base.singleton_class.send(:prepend, shift_cx_modulizer)
47
52
  end
48
53
 
54
+ # NOTE: Possible difference in how inheritance works when using extend vs include
55
+ # with Shiftable::Collection.new
49
56
  def included(base)
50
- shift_cx_modulizer = ShiftCollectionModulizer.to_mod(@belongs_to, @has_many, @method_prefix,
51
- @before_save)
57
+ shift_cx_modulizer = ShiftCollectionModulizer.to_mod(@signature.add_base(base))
52
58
  base.send(:prepend, shift_cx_modulizer)
53
59
  end
54
60
 
55
61
  # Creates anonymous Ruby Modules, containing dynamically built methods
56
62
  module ShiftCollectionModulizer
57
- def to_mod(belongs_to, has_many, mepr, before_save)
63
+ def to_mod(signature)
64
+ prefix = signature.method_prefix
58
65
  Module.new do
59
- define_method(:"#{mepr}shift_cx_column") do
60
- reflection = reflect_on_association(belongs_to).klass.reflect_on_association(has_many)
61
- reflection.foreign_key
66
+ define_method(:"#{prefix}shift_cx_column") do
67
+ signature.shift_column
62
68
  end
63
- define_method(:"#{mepr}shift_cx_relation") do |id|
64
- return nil unless id
65
-
66
- where(send("#{mepr}shift_cx_column") => id)
67
- end
68
- define_method(:"#{mepr}shift_cx_safe_relation") do |shift_to:, shift_from:|
69
- return false if shift_from&.id.nil?
70
- return false if shift_to&.id.nil?
71
-
72
- send("#{mepr}shift_cx_relation", shift_from.id)
73
- end
74
- define_method(:"#{mepr}shift_cx") do |shift_to:, shift_from:|
75
- shifting_rel = send("#{mepr}shift_cx_safe_relation", shift_to: shift_to, shift_from: shift_from)
76
- return false unless shifting_rel && shifting_rel.any?
77
-
78
- shifting_rel.each do |shifting|
79
- shifting.send("#{send("#{mepr}shift_cx_column")}=", shift_to.id)
80
- end
81
- before_save.call(shifting_rel: shifting_rel, shift_to: shift_to, shift_from: shift_from) if before_save
82
- shifting_rel.each do |shifting|
83
- shifting.save
84
- end
85
- shifting_rel
69
+ define_method(:"#{prefix}shift_cx") do |shift_to:, shift_from:|
70
+ signature.shift_data!(shift_to: shift_to, shift_from: shift_from)
86
71
  end
87
72
  end
88
73
  end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shiftable
4
+ class ModSignature
5
+ VALID_TYPES = %i[sg cx].freeze
6
+ VALID_ASSOCIATIONS = {
7
+ sg: %i[belongs_to has_one],
8
+ cx: %i[belongs_to has_many]
9
+ }.freeze
10
+ DEFAULT_BEFORE_SHIFT = ->(*_) { true }
11
+ attr_reader :associations, :options, :type, :base
12
+
13
+ # Imagine you are a Spaceship Captain, the Spaceship belongs_to you, and it has only one Captain.
14
+ # But you have to sell it to your nemesis!
15
+ def initialize(associations:, type:, options: {})
16
+ @associations = associations
17
+ @options = options
18
+ @type = type
19
+ # See: https://maximomussini.com/posts/practical-applications-of-the-singleton-class/
20
+ singleton_class.send(:prepend, Object.const_get("Shiftable::ModSignature::#{type.capitalize}Methods", false))
21
+ validate
22
+ end
23
+
24
+ def validate
25
+ raise ArgumentError, "type must be one of: #{VALID_TYPES}, provided: #{type}" if invalid_type?
26
+ raise ArgumentError, "associations must be symbols" if invalid_association_key_type?
27
+ raise ArgumentError, "exactly two distinct associations must be provided" if invalid_number_of_associations?
28
+ end
29
+
30
+ def invalid_type?
31
+ !VALID_TYPES.include?(type)
32
+ end
33
+
34
+ def invalid_association_key_type?
35
+ associations.keys.detect { |key| !key.is_a?(Symbol) }
36
+ end
37
+
38
+ def invalid_number_of_associations?
39
+ associations.keys.uniq.length != 2
40
+ end
41
+
42
+ # @note Chainable
43
+ # @return self
44
+ def add_base(base)
45
+ @base = base
46
+ self
47
+ end
48
+
49
+ def has_rel_name
50
+ VALID_ASSOCIATIONS[type][1]
51
+ end
52
+
53
+ def validate_relationships
54
+ bt = base.reflect_on_association(belongs_to)
55
+ raise ArgumentError, "Unable to find belongs_to: :#{belongs_to} in #{base}" unless bt
56
+
57
+ klass = bt.klass
58
+ hr = klass.reflect_on_association(has_rel)
59
+ raise ArgumentError, "Unable to find #{has_rel_name}: :#{has_rel} in #{klass}" unless hr
60
+ end
61
+
62
+ module CxMethods
63
+ def has_many
64
+ associations[:has_many]
65
+ end
66
+
67
+ alias has_rel has_many
68
+
69
+ def shift_data!(shift_to:, shift_from:)
70
+ validate_relationships
71
+ shifting_rel = ShiftingRelation.new(
72
+ to: shift_to,
73
+ from: shift_from,
74
+ column: shift_column,
75
+ base: base
76
+ )
77
+ shifting_rel.shift do |result|
78
+ before_shift&.call(shifting_rel: result, shift_to: shift_to, shift_from: shift_from)
79
+ end
80
+ end
81
+ end
82
+
83
+ module SgMethods
84
+ def has_one
85
+ associations[:has_one]
86
+ end
87
+
88
+ alias has_rel has_one
89
+
90
+ # Do not move record if a record already exists (we are shifting a "has_one" association, after all)
91
+ def precheck
92
+ options[:precheck]
93
+ end
94
+
95
+ def shift_data!(shift_to:, shift_from:)
96
+ validate_relationships
97
+ shifting = ShiftingRecord.new(
98
+ to: shift_to,
99
+ from: shift_from,
100
+ column: shift_column,
101
+ base: base
102
+ ) do
103
+ !precheck || !shift_to.send(has_one)
104
+ end
105
+ shifting.shift do |result|
106
+ before_shift&.call(shifting: result, shift_to: shift_to, shift_from: shift_from)
107
+ end
108
+ end
109
+ end
110
+
111
+ # The name of the belongs_to association, defined on the shifting model, e.g. Spaceship
112
+ # Normally a camel-cased, symbolized, version of the class name.
113
+ # In the case where Spaceship belongs_to: :captain, this is :captain.
114
+ def belongs_to
115
+ associations[:belongs_to]
116
+ end
117
+
118
+ def method_prefix
119
+ options[:method_prefix]
120
+ end
121
+
122
+ # will prevent the save if it returns false
123
+ # allows for any custom logic to be run, such as setting shift_from attributes, prior to the shift is saved.
124
+ def before_shift
125
+ options[:before_shift] || DEFAULT_BEFORE_SHIFT
126
+ end
127
+
128
+ def shift_column
129
+ reflection = base.reflect_on_association(belongs_to).klass.reflect_on_association(has_rel)
130
+ reflection.foreign_key
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shiftable
4
+ # Gets data to be shifted
5
+ class Shifting
6
+ attr_reader :to, :from, :column, :base, :result, :run_save
7
+
8
+ def initialize(to:, from:, column:, base:)
9
+ @to = to
10
+ @from = from
11
+ @column = column
12
+ @base = base
13
+ validate
14
+ do_query = block_given? ? yield : true
15
+ @result = do_query ? query : nil
16
+ @run_save = true
17
+ end
18
+
19
+ # def found?
20
+ # raise "found? must be defined in a subclass"
21
+ # end
22
+
23
+ # def shift
24
+ # raise "shift must be defined in a subclass"
25
+ # end
26
+
27
+ private
28
+
29
+ def validate
30
+ raise ArgumentError, "shift_to must have an id (primary key) value, but is: #{to&.id}" unless to&.id
31
+ raise ArgumentError, "shift_from must have an id (primary key) value, but is: #{from&.id}" unless from&.id
32
+ end
33
+
34
+ # def query
35
+ # raise "query must be defined in a subclass"
36
+ # end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shiftable
4
+ # Gets data to be shifted
5
+ class ShiftingRecord < Shifting
6
+ def found?
7
+ !!result
8
+ end
9
+
10
+ # @return true, false
11
+ def shift
12
+ return false unless found?
13
+
14
+ result.send("#{column}=", to.id)
15
+ @run_save = yield result if block_given?
16
+ result.save if run_save
17
+ end
18
+
19
+ private
20
+
21
+ def query
22
+ base.find_by(column => from.id)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shiftable
4
+ # Gets data to be shifted
5
+ class ShiftingRelation < Shifting
6
+ include Enumerable
7
+
8
+ def found?
9
+ result.any?
10
+ end
11
+
12
+ def each(&block)
13
+ result.each(&block)
14
+ end
15
+
16
+ # @return result (once it is shifted)
17
+ def shift
18
+ return false unless found?
19
+
20
+ each do |record|
21
+ record.send("#{column}=", to.id)
22
+ end
23
+ @run_save = yield result if block_given?
24
+ each(&:save) if run_save
25
+ result
26
+ end
27
+
28
+ private
29
+
30
+ def query
31
+ base.where(column => from.id)
32
+ end
33
+ end
34
+ end