shiftable 0.1.1 → 0.4.1

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: 79602092714ac55294711f0a60012290a85b20f46039eec63a2b698fcd433f0e
4
- data.tar.gz: 61e2a2c47e9afe4c28b518d4ec6dc017e3a3441f2f7c513f17cc9630f04bb7cf
3
+ metadata.gz: 7792e05d55078e47ad4fec714ebc5aeed51c6373ba64d31e410ab8cdfe01eb44
4
+ data.tar.gz: d1435eb2ab87f9e81367d73036d8bf23c270eccb29157cc1eaf364da9ef8fcb2
5
5
  SHA512:
6
- metadata.gz: d14b60642c03f4576cfae6030efffbe8de8dc9b1ea0b9d7db9ccb7ceea88f2de2582510918998e2e82ef146ca6adaf94d4f2eb9c10522363d8f705bcc24c6747
7
- data.tar.gz: 6d4793fc03014121e8c4ab2eaf369bbdadb892c94b52c897b25b05c1c2903c82310c6256fc5ba57c47df688f15147294b7a3236994f477196bd747570165c87a
6
+ metadata.gz: 8fc584eefc4ab6e4bbdcbb22a3b856f8baa2dff98daf5eceb149ced8865fa5c1dc020bcf1cde2918df47f3b0545c9c1827e9f369e2131c4bb5498804c1cce37c
7
+ data.tar.gz: d7970f2f74a80bdd5a657e89846a5eddd54d7396cd000d3022b0ba1782336b464586ea1391a23af6ff8df6b7d6047715c571cdfb43d55145744504f5ced7e4db
data/CHANGELOG.md CHANGED
@@ -1,14 +1,39 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2021-10-23
3
+ ## [0.4.0] - 2021-10-27
4
+
5
+ ### Changed
6
+
7
+ - option :preflight_checks renamed to :precheck
4
8
 
5
9
  ### Added
6
10
 
7
- - Initial release
8
- - feat: supports shifting of records associated as has_one / belongs_to and has_many / belongs_to, including with STI.
9
- - 100% test coverage
11
+ - Even more 100% spec coverage
10
12
 
11
- ## [0.1.0] - 2021-10-23
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
12
37
 
13
38
  ### Fixed
14
39
 
@@ -18,5 +43,20 @@
18
43
 
19
44
  - Linting
20
45
 
46
+ ## [0.1.0] - 2021-10-23
47
+
48
+ ### Added
49
+
50
+ - Initial release
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
+
21
54
  [0.1.0]: https://github.com/pboling/shiftable/releases/tag/v0.1.0
22
- [0.1.1]: https://github.com/pboling/shiftable/releases/tag/v0.1.1
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
61
+
62
+ [0.4.0]: https://github.com/pboling/shiftable/releases/tag/v0.4.0
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,25 @@
1
+ ## Contributing
2
+
3
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/pboling/activerecord-transactionable][source]
4
+ . This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to
5
+ the [code of conduct][conduct].
6
+
7
+ To submit a patch, please fork the project and create a patch with tests. Once you're happy with it send a pull request!
8
+
9
+ ## Contributors
10
+
11
+ [![Contributors](https://contrib.rocks/image?repo=pboling/activerecord-transactionable)][contributors]
12
+
13
+ Made with [contributors-img][contrib-rocks].
14
+
15
+ [comment]: <> (Following links are used by README, CONTRIBUTING)
16
+
17
+ [conduct]: https://github.com/pboling/activerecord-transactionable/blob/master/CODE_OF_CONDUCT.md
18
+
19
+ [contrib-rocks]: https://contrib.rocks
20
+
21
+ [contributors]: https://github.com/pboling/activerecord-transactionable/graphs/contributors
22
+
23
+ [comment]: <> (Following links are used by README, CONTRIBUTING, Homepage)
24
+
25
+ [source]: https://github.com/pboling/activerecord-transactionable/
data/README.md CHANGED
@@ -5,12 +5,30 @@ 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
+ | name, license, docs | [![RubyGems.org](https://img.shields.io/badge/name-shiftable-brightgreen.svg?style=flat)][rubygems] [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)][license-ref] [![RubyDoc.info](https://img.shields.io/badge/documentation-rubydoc-brightgreen.svg?style=flat)][documentation] |
11
+ | version & downloads | [![Version](https://img.shields.io/gem/v/shiftable.svg)][rubygems] [![Total Downloads](https://img.shields.io/gem/dt/shiftable.svg)][rubygems] [![Downloads Today](https://img.shields.io/gem/rd/shiftable.svg)][rubygems] [![Homepage](https://img.shields.io/badge/source-github-brightgreen.svg?style=flat)][source] |
12
+ | dependencies & linting | [![Depfu](https://badges.depfu.com/badges/0412727b7e3b740b950a683eebc708e2/count.svg)][depfu] [![lint status](https://github.com/pboling/shiftable/actions/workflows/style.yml/badge.svg)][actions] |
13
+ | unit tests | [![supported rubies](https://github.com/pboling/shiftable/actions/workflows/supported.yml/badge.svg)][actions] [![unsupported status](https://github.com/pboling/shiftable/actions/workflows/unsupported.yml/badge.svg)][actions] |
14
+ | coverage & maintainability | [![Test Coverage](https://api.codeclimate.com/v1/badges/a53aa8b7c413b950d519/test_coverage)][climate_coverage] [![codecov](https://codecov.io/gh/pboling/shiftable/branch/main/graph/badge.svg?token=J1542PYN2Z)][codecov_coverage] [![Maintainability](https://api.codeclimate.com/v1/badges/a53aa8b7c413b950d519/maintainability)][climate_maintainability] [![Security Policy](https://img.shields.io/badge/security-policy-brightgreen.svg?style=flat)][security] |
15
+ | resources | [![Discussion](https://img.shields.io/badge/discussions-github-brightgreen.svg?style=flat)][gh_discussions] [![Get help on Codementor](https://cdn.codementor.io/badges/get_help_github.svg)](https://www.codementor.io/peterboling?utm_source=github&utm_medium=button&utm_term=peterboling&utm_campaign=github) [![Join the chat at https://gitter.im/pboling/shiftable](https://badges.gitter.im/Join%20Chat.svg)][chat] [![Blog](https://img.shields.io/badge/blog-railsbling-brightgreen.svg?style=flat)][blogpage] |
16
+ | Spread ~♡ⓛⓞⓥⓔ♡~ | [![Open Source Helpers](https://www.codetriage.com/pboling/shiftable/badges/users.svg)][code_triage] [![Liberapay Patrons](https://img.shields.io/liberapay/patrons/pboling.svg?logo=liberapay)][liberapay_donate] [![Sponsor Me](https://img.shields.io/badge/sponsor-pboling.svg?style=social&logo=github)][gh_sponsors] [🌏][aboutme] [👼][angelme] [💻][coderme] [🌹][politicme] [![Tweet @ Peter][followme-img]][tweetme] |
17
+
18
+ ## Compatibility
19
+
20
+ Targeted ruby compatibility is non-EOL versions of Ruby, currently 2.6, 2.7, and 3.0, but may work on older Rubies back
21
+ to 2.0, though it is limited to 2.5 in the gemspec. Feel free to fork if you need something older! Targeted ActiveRecord
22
+ (Rails not required) compatibility follows the same scheme
23
+ as [Rails Security Issue maintenance policy][maintenancee_policy], currently 6.1, 6.0, 5.2, but it is highly likely that
24
+ this code will work in any version of ActiveRecord/Rails that runs on Ruby 2+.
25
+
8
26
  ## Installation
9
27
 
10
28
  Add this line to your application's Gemfile:
11
29
 
12
30
  ```ruby
13
- gem 'shiftable'
31
+ gem "shiftable"
14
32
  ```
15
33
 
16
34
  And then execute:
@@ -61,32 +79,86 @@ But how can you accomplish this? If you used the `shiftable` gem, won't take but
61
79
 
62
80
  class Spaceship < ActiveRecord::Base
63
81
  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
- )
82
+ extend Shiftable::Single.new belongs_to: :captain, has_one: :spaceship, precheck: true,
83
+ before_shift: ->(shifting:, shift_to:, shift_from:) { shifting.ownership_changes += 1 }
84
+ end
85
+ ```
86
+
87
+ 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...
88
+
89
+ ```ruby
90
+
91
+ class Spaceship < ActiveRecord::Base
92
+ belongs_to :captain
93
+
94
+ class << self
95
+ include Shiftable::Single.new belongs_to: :captain, has_one: :spaceship, precheck: true,
96
+ before_shift: lambda { |shifting:, shift_to:, shift_from:|
97
+ shifting.ownership_changes += 1
98
+ }
99
+ end
70
100
  end
71
101
  ```
72
102
 
103
+ ### Single Table Inheritance
104
+
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
+
73
107
  ### Multiple association on a single class
74
108
 
75
- What if the Captain is really all that though? What if the captain and the spaceship have a boss... the space
76
- federation!
109
+ What if the captain and the spaceship have a boss... the space
110
+ federation! And in a run-in with their arch-Nemesis the Plinth-inth,
111
+ all federation spaceships are commandeered! You are ruined!
112
+
113
+ ```ruby
114
+
115
+ class Spaceship < ActiveRecord::Base
116
+ belongs_to :space_federation
117
+ extend Shiftable::Collection.new belongs_to: :space_federation, has_one: :spaceship,
118
+ before_shift: lambda { |shifting_rel:, shift_to:, shift_from:|
119
+ shifting_rel.each { |spaceship| spaceship.federation_changes += 1 }
120
+ }
121
+ end
122
+
123
+ class SpaceFederation < ActiveRecord::Base
124
+ has_many :spaceships
125
+
126
+ def all_spaceships_commandeered_by(nemesis_federation)
127
+ Spaceship.shift_cx(shift_to: nemesis_federation, shift_from: self)
128
+ end
129
+ end
130
+ ```
131
+
132
+ ### Complete example
133
+
134
+ Putting it all together...
77
135
 
78
136
  ```ruby
137
+ class Captain < ActiveRecord::Base
138
+ has_one :spaceship
139
+
140
+ def sell_spaceship_to(nemesis_captain)
141
+ Spaceship.shift_single(shift_to: nemesis_captain, shift_from: self)
142
+ end
143
+ end
79
144
 
80
145
  class Spaceship < ActiveRecord::Base
81
146
  belongs_to :captain
147
+ extend Shiftable::Single.new belongs_to: :captain, has_one: :spaceship, precheck: true,
148
+ before_shift: ->(shifting:, shift_to:, shift_from:) { shifting.ownership_changes += 1 }
149
+
82
150
  belongs_to :space_federation
151
+ extend Shiftable::Collection.new belongs_to: :space_federation, has_one: :spaceship,
152
+ before_shift: lambda { |shifting_rel:, shift_to:, shift_from:|
153
+ shifting_rel.each { |spaceship| spaceship.federation_changes += 1 }
154
+ }
83
155
  end
84
156
 
85
157
  class SpaceFederation < ActiveRecord::Base
86
158
  has_many :captains
87
159
  has_many :spaceships
88
160
 
89
- def sell_spaceship_to(nemesis_federation)
161
+ def all_spaceships_commandeered_by(nemesis_federation)
90
162
  Spaceship.shift_cx(shift_to: nemesis_federation, shift_from: self)
91
163
  end
92
164
  end
@@ -94,6 +166,21 @@ end
94
166
 
95
167
  ... stay tuned!
96
168
 
169
+ ## More Information
170
+
171
+ * RubyDoc
172
+ Documentation: [![RubyDoc.info](https://img.shields.io/badge/documentation-rubydoc-brightgreen.svg?style=flat)][documentation]
173
+ * GitHub
174
+ Discussions: [![Discussion](https://img.shields.io/badge/discussions-github-brightgreen.svg?style=flat)][gh_discussions]
175
+ * Live Chat on
176
+ Gitter: [![Join the chat at https://gitter.im/pboling/activerecord-transactionable](https://badges.gitter.im/Join%20Chat.svg)][chat]
177
+ * Maintainer's Blog: [![Blog](https://img.shields.io/badge/blog-railsbling-brightgreen.svg?style=flat)][blogpage]
178
+
179
+ ## Code of Conduct
180
+
181
+ Everyone interacting in the Shiftable project's codebases, issue trackers, chat rooms and mailing lists is expected to
182
+ follow the [code of conduct][conduct].
183
+
97
184
  ## Development
98
185
 
99
186
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can
@@ -105,12 +192,104 @@ push git commits and the created tag, and push the `.gem` file to [rubygems.org]
105
192
 
106
193
  ## Contributing
107
194
 
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).
195
+ See [CONTRIBUTING.md][contributing]
196
+
197
+ ## Contributors
198
+
199
+ [![Contributors](https://contrib.rocks/image?repo=pboling/shiftable)]("https://github.com/pboling/shiftable/graphs/contributors")
200
+
201
+ Made with [contributors-img](https://contrib.rocks).
202
+
203
+ ## Versioning
204
+
205
+ This library aims to adhere to [Semantic Versioning 2.0.0][semver]. Violations of this scheme should be reported as
206
+ bugs. Specifically, if a minor or patch version is released that breaks backward compatibility, a new version should be
207
+ immediately released that restores compatibility. Breaking changes to the public API will only be introduced with new
208
+ major versions.
209
+
210
+ As a result of this policy, you can (and should) specify a dependency on this gem using
211
+ the [Pessimistic Version Constraint][pvc] with two digits of precision.
212
+
213
+ For example:
214
+
215
+ ```ruby
216
+ spec.add_dependency "shiftable", "~> 0.4"
217
+ ```
218
+
219
+ ## Contact
220
+
221
+ Author and maintainer is Peter Boling ([@pboling][gh_sponsors]).
222
+
223
+ Comments are welcome in the [GitHub Discussions][gh_discussions] board.
224
+
225
+ For security-related issues see [SECURITY][security].
109
226
 
110
227
  ## License
111
228
 
112
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
229
+ The gem is available as open source under the terms of
230
+ the [MIT License][license] [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)][license-ref].
231
+ See [LICENSE][license] for the official [Copyright Notice][copyright-notice-explainer].
113
232
 
114
- ## Code of Conduct
233
+ * Copyright (c) 2021 [Peter H. Boling][peterboling] of [Rails Bling][railsbling]
234
+
235
+ [copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
236
+
237
+ [gh_discussions]: https://github.com/pboling/shiftable/discussions
238
+
239
+ [conduct]: https://github.com/pboling/shiftable/blob/master/CODE_OF_CONDUCT.md
240
+
241
+ [security]: https://github.com/pboling/shiftable/blob/master/SECURITY.md
242
+
243
+ [license]: https://github.com/pboling/shiftable/blob/master/LICENSE.txt
244
+
245
+ [license-ref]: https://opensource.org/licenses/MIT
246
+
247
+ [semver]: http://semver.org/
248
+
249
+ [pvc]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint
250
+
251
+ [railsbling]: http://www.railsbling.com
252
+
253
+ [peterboling]: http://www.peterboling.com
254
+
255
+ [aboutme]: https://about.me/peter.boling
256
+
257
+ [angelme]: https://angel.co/peter-boling
258
+
259
+ [coderme]:http://coderwall.com/pboling
260
+
261
+ [followme-img]: https://img.shields.io/twitter/follow/galtzo.svg?style=social&label=Follow
262
+
263
+ [tweetme]: http://twitter.com/galtzo
264
+
265
+ [politicme]: https://nationalprogressiveparty.org
266
+
267
+ [documentation]: https://rubydoc.info/github/pboling/shiftable/main
268
+
269
+ [source]: https://github.com/pboling/shiftable/
270
+
271
+ [actions]: https://github.com/pboling/shiftable/actions
272
+
273
+ [issues]: https://github.com/pboling/shiftable/issues
274
+
275
+ [climate_maintainability]: https://codeclimate.com/github/pboling/shiftable/maintainability
276
+
277
+ [climate_coverage]: https://codeclimate.com/github/pboling/shiftable/test_coverage
278
+
279
+ [codecov_coverage]: https://codecov.io/gh/pboling/shiftable
280
+
281
+ [code_triage]: https://www.codetriage.com/pboling/shiftable
282
+
283
+ [depfu]: https://depfu.com/github/pboling/shiftable?project_id=32594
284
+
285
+ [blogpage]: http://www.railsbling.com/tags/shiftable/
286
+
287
+ [rubygems]: https://rubygems.org/gems/shiftable
288
+
289
+ [chat]: https://gitter.im/pboling/shiftable?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
290
+
291
+ [maintenancee_policy]: https://guides.rubyonrails.org/maintenance_policy.html#security-issues
292
+
293
+ [liberapay_donate]: https://liberapay.com/pboling/donate
115
294
 
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).
295
+ [gh_sponsors]: https://github.com/sponsors/pboling
@@ -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,72 +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)
82
- shifting_rel.each(&:save)
83
- 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)
84
71
  end
85
72
  end
86
73
  end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shiftable
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
7
+ DEFAULT_BEFORE_SHIFT = ->(*_) { true }
8
+ attr_reader :associations, :options, :type, :base
9
+
10
+ # Imagine you are a Spaceship Captain, the Spaceship belongs_to you, and it has only one Captain.
11
+ # But you have to sell it to your nemesis!
12
+ def initialize(associations:, type:, options: {})
13
+ @associations = associations
14
+ @options = options
15
+ @type = type
16
+ # See: https://maximomussini.com/posts/practical-applications-of-the-singleton-class/
17
+ singleton_class.send(:prepend, Object.const_get("Shiftable::ModSignature::#{type.capitalize}Methods", false))
18
+ validate
19
+ end
20
+
21
+ def validate
22
+ raise ArgumentError, "type must be one of: #{VALID_TYPES}, provided: #{type}" if invalid_type?
23
+ raise ArgumentError, "associations must be symbols" if invalid_association_key_type?
24
+ raise ArgumentError, "exactly two distinct associations must be provided" if invalid_number_of_associations?
25
+ end
26
+
27
+ def invalid_type?
28
+ !VALID_TYPES.include?(type)
29
+ end
30
+
31
+ def invalid_association_key_type?
32
+ associations.keys.detect { |key| !key.is_a?(Symbol) }
33
+ end
34
+
35
+ def invalid_number_of_associations?
36
+ associations.keys.uniq.length != 2
37
+ end
38
+
39
+ # @note Chainable
40
+ # @return self
41
+ def add_base(base)
42
+ @base = base
43
+ self
44
+ end
45
+
46
+ def has_rel_name
47
+ VALID_ASSOCIATIONS[type][1]
48
+ end
49
+
50
+ 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
57
+ end
58
+
59
+ module CxMethods
60
+ def has_many
61
+ associations[:has_many]
62
+ end
63
+
64
+ alias has_rel has_many
65
+
66
+ def shift_data!(shift_to:, shift_from:)
67
+ validate_relationships
68
+ shifting_rel = ShiftingRelation.new(
69
+ to: shift_to,
70
+ from: shift_from,
71
+ column: shift_column,
72
+ base: base
73
+ )
74
+ shifting_rel.shift do |result|
75
+ before_shift&.call(shifting_rel: result, shift_to: shift_to, shift_from: shift_from)
76
+ end
77
+ end
78
+ end
79
+
80
+ module SgMethods
81
+ def has_one
82
+ associations[:has_one]
83
+ end
84
+
85
+ alias has_rel has_one
86
+
87
+ # Do not move record if a record already exists (we are shifting a "has_one" association, after all)
88
+ def precheck
89
+ options[:precheck]
90
+ end
91
+
92
+ def shift_data!(shift_to:, shift_from:)
93
+ validate_relationships
94
+ shifting = ShiftingRecord.new(
95
+ to: shift_to,
96
+ from: shift_from,
97
+ column: shift_column,
98
+ base: base
99
+ ) do
100
+ !precheck || !shift_to.send(has_one)
101
+ end
102
+ shifting.shift do |result|
103
+ before_shift&.call(shifting: result, shift_to: shift_to, shift_from: shift_from)
104
+ end
105
+ end
106
+ end
107
+
108
+ # The name of the belongs_to association, defined on the shifting model, e.g. Spaceship
109
+ # Normally a camel-cased, symbolized, version of the class name.
110
+ # In the case where Spaceship belongs_to: :captain, this is :captain.
111
+ def belongs_to
112
+ associations[:belongs_to]
113
+ end
114
+
115
+ def method_prefix
116
+ options[:method_prefix]
117
+ end
118
+
119
+ # will prevent the save if it returns false
120
+ # allows for any custom logic to be run, such as setting shift_from attributes, prior to the shift is saved.
121
+ def before_shift
122
+ options[:before_shift] || DEFAULT_BEFORE_SHIFT
123
+ end
124
+
125
+ def shift_column
126
+ reflection = base.reflect_on_association(belongs_to).klass.reflect_on_association(has_rel)
127
+ reflection.foreign_key
128
+ end
129
+ end
130
+ 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