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 +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](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
|
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: [![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
|
-
|
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
|
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
|
-
|
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
|