shiftable 0.1.0 → 0.4.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
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