shiftable 0.1.1 → 0.4.1

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: 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