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 +4 -4
- data/CHANGELOG.md +46 -6
- data/CONTRIBUTING.md +25 -0
- data/README.md +193 -14
- data/lib/shiftable/collection.rb +38 -51
- data/lib/shiftable/mod_signature.rb +130 -0
- data/lib/shiftable/shifting.rb +38 -0
- data/lib/shiftable/shifting_record.rb +25 -0
- data/lib/shiftable/shifting_relation.rb +34 -0
- data/lib/shiftable/single.rb +38 -55
- data/lib/shiftable/version.rb +1 -1
- data/lib/shiftable.rb +5 -1
- metadata +41 -113
- data/.rspec +0 -3
- data/.rubocop.yml +0 -100
- data/.rubocop_todo.yml +0 -67
- data/Gemfile +0 -6
- data/Rakefile +0 -12
- data/bin/console +0 -15
- data/bin/setup +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7792e05d55078e47ad4fec714ebc5aeed51c6373ba64d31e410ab8cdfe01eb44
|
4
|
+
data.tar.gz: d1435eb2ab87f9e81367d73036d8bf23c270eccb29157cc1eaf364da9ef8fcb2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8fc584eefc4ab6e4bbdcbb22a3b856f8baa2dff98daf5eceb149ced8865fa5c1dc020bcf1cde2918df47f3b0545c9c1827e9f369e2131c4bb5498804c1cce37c
|
7
|
+
data.tar.gz: d7970f2f74a80bdd5a657e89846a5eddd54d7396cd000d3022b0ba1782336b464586ea1391a23af6ff8df6b7d6047715c571cdfb43d55145744504f5ced7e4db
|
data/CHANGELOG.md
CHANGED
@@ -1,14 +1,39 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
-
## [0.
|
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
|
-
-
|
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.
|
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
|
-
|
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]
|
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] [][license-ref] [][documentation] |
|
11
|
+
| version & downloads | [][rubygems] [][rubygems] [][rubygems] [][source] |
|
12
|
+
| dependencies & linting | [][depfu] [][actions] |
|
13
|
+
| unit tests | [][actions] [][actions] |
|
14
|
+
| coverage & maintainability | [][climate_coverage] [][codecov_coverage] [][climate_maintainability] [][security] |
|
15
|
+
| resources | [][gh_discussions] [](https://www.codementor.io/peterboling?utm_source=github&utm_medium=button&utm_term=peterboling&utm_campaign=github) [][chat] [][blogpage] |
|
16
|
+
| Spread ~♡ⓛⓞⓥⓔ♡~ | [][code_triage] [][liberapay_donate] [][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
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
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
|
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: [][documentation]
|
173
|
+
* GitHub
|
174
|
+
Discussions: [][gh_discussions]
|
175
|
+
* Live Chat on
|
176
|
+
Gitter: [][chat]
|
177
|
+
* Maintainer's Blog: [][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
|
-
|
195
|
+
See [CONTRIBUTING.md][contributing]
|
196
|
+
|
197
|
+
## Contributors
|
198
|
+
|
199
|
+
[]("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
|
229
|
+
The gem is available as open source under the terms of
|
230
|
+
the [MIT License][license] [][license-ref].
|
231
|
+
See [LICENSE][license] for the official [Copyright Notice][copyright-notice-explainer].
|
113
232
|
|
114
|
-
|
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
|
-
|
295
|
+
[gh_sponsors]: https://github.com/sponsors/pboling
|
data/lib/shiftable/collection.rb
CHANGED
@@ -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
|
-
|
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
|
-
@
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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(@
|
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(@
|
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(
|
63
|
+
def to_mod(signature)
|
64
|
+
prefix = signature.method_prefix
|
58
65
|
Module.new do
|
59
|
-
define_method(:"#{
|
60
|
-
|
61
|
-
reflection.foreign_key
|
66
|
+
define_method(:"#{prefix}shift_cx_column") do
|
67
|
+
signature.shift_column
|
62
68
|
end
|
63
|
-
define_method(:"#{
|
64
|
-
|
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
|