shiftable 0.1.0 → 0.4.0
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 +54 -2
- data/README.md +145 -14
- data/lib/shiftable/collection.rb +38 -53
- data/lib/shiftable/mod_signature.rb +133 -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 +139 -58
- data/.rspec +0 -3
- data/.rubocop.yml +0 -100
- data/.rubocop_todo.yml +0 -46
- 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: 62926c718edd257728210a0870ea5eca44aeb35b33a929d87d2efb8a5f902910
|
4
|
+
data.tar.gz: b4c35fcc2a3eff45d992b5260e6d4f2f13d870782f40d06285172eb2f1207f9d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c21a1f0c51b4a3cb715730fe07bb29d562a84d8dd79b558a3ca06c6a7bf20032928225b41aa1e749b4a7a85afe4fdb33cbf9d068b7539ef622665272f1f8115
|
7
|
+
data.tar.gz: d9bc2d54fe6fb3f15f781b33ddf83c6abfa91f7b66e426274bc9ef07595e61a9c6b4f42a2ffe27e6b8cce5453dbcc0221ac07261bddb762a3a9eba3e79d7cef1
|
data/CHANGELOG.md
CHANGED
@@ -1,10 +1,62 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.4.0] - 2021-10-27
|
4
|
+
|
5
|
+
### Changed
|
6
|
+
|
7
|
+
- option :preflight_checks renamed to :precheck
|
8
|
+
|
9
|
+
### Added
|
10
|
+
|
11
|
+
- Even more 100% spec coverage
|
12
|
+
|
13
|
+
## [0.3.0] - 2021-10-26
|
14
|
+
|
15
|
+
### Changed
|
16
|
+
|
17
|
+
- Internal rewrite to improve maintainability
|
18
|
+
- Extreme edge cases involving incorrect configuration will raise slightly different errors.
|
19
|
+
|
20
|
+
### Added
|
21
|
+
|
22
|
+
- Even more 100% spec coverage
|
23
|
+
|
24
|
+
## [0.2.0] - 2021-10-24
|
25
|
+
|
26
|
+
### Changed
|
27
|
+
|
28
|
+
- option `before_save` is now `before_shift` as originally documented
|
29
|
+
|
30
|
+
### Updated
|
31
|
+
|
32
|
+
- Github Actions now test all supported Rubies
|
33
|
+
- Linting
|
34
|
+
- Documentation
|
35
|
+
|
36
|
+
## [0.1.1] - 2021-10-23
|
37
|
+
|
38
|
+
### Fixed
|
39
|
+
|
40
|
+
- Github Actions build
|
41
|
+
|
42
|
+
### Updated
|
43
|
+
|
44
|
+
- Linting
|
45
|
+
|
3
46
|
## [0.1.0] - 2021-10-23
|
4
47
|
|
5
48
|
### Added
|
6
49
|
|
7
50
|
- Initial release
|
8
|
-
- feat: supports shifting of records associated as has_one / belongs_to
|
51
|
+
- feat: supports shifting of records associated as has_one / belongs_to and has_many / belongs_to, including with STI.
|
52
|
+
- 100% test coverage
|
53
|
+
|
54
|
+
[0.1.0]: https://github.com/pboling/shiftable/releases/tag/v0.1.0
|
55
|
+
|
56
|
+
[0.1.1]: https://github.com/pboling/shiftable/releases/tag/v0.1.1
|
57
|
+
|
58
|
+
[0.2.0]: https://github.com/pboling/shiftable/releases/tag/v0.2.0
|
59
|
+
|
60
|
+
[0.3.0]: https://github.com/pboling/shiftable/releases/tag/v0.3.0
|
9
61
|
|
10
|
-
[0.
|
62
|
+
[0.4.0]: https://github.com/pboling/shiftable/releases/tag/v0.4.0
|
data/README.md
CHANGED
@@ -5,12 +5,38 @@ Do your Spaceships belong to Captains, but sometimes a Captain will retire, and
|
|
5
5
|
We've all been there. This gem provides structure around the process of "shifting" your records from one associated
|
6
6
|
record to a new record.
|
7
7
|
|
8
|
+
| Project | Shiftable |
|
9
|
+
|------------------------ | ----------------------- |
|
10
|
+
| gem name | [shiftable](https://rubygems.org/gems/shiftable) |
|
11
|
+
| license | [](https://opensource.org/licenses/MIT) |
|
12
|
+
| download rank | [](https://github.com/pboling/shiftable) |
|
13
|
+
| version | [](https://rubygems.org/gems/shiftable) |
|
14
|
+
| dependencies | [](https://depfu.com/github/pboling/shiftable?project_id=32594) |
|
15
|
+
| unit tests | [](https://github.com/pboling/shiftable/actions) |
|
16
|
+
| lint status | [](https://github.com/pboling/shiftable/actions) |
|
17
|
+
| unsupported status | [](https://github.com/pboling/shiftable/actions) |
|
18
|
+
| test coverage | [](https://codeclimate.com/github/pboling/shiftable/test_coverage) |
|
19
|
+
| maintainability | [](https://codeclimate.com/github/pboling/shiftable/maintainability) |
|
20
|
+
| code triage | [](https://www.codetriage.com/pboling/shiftable) |
|
21
|
+
| homepage | [on Github.com][homepage], [on Railsbling.com][blogpage] |
|
22
|
+
| documentation | [on RDoc.info][documentation] |
|
23
|
+
| Spread ~♡ⓛⓞⓥⓔ♡~ | [🌏][aboutme], [👼][angelme], [💻][coderme], [![Tweet @ Peter][followme-img]][tweetme], [🌹][politicme] |
|
24
|
+
|
25
|
+
## Compatibility
|
26
|
+
|
27
|
+
Targeted ruby compatibility is non-EOL versions of Ruby, currently 2.6, 2.7, and 3.0, but may work on older Rubies back
|
28
|
+
to 2.0, though it is limited to 2.5 in the gemspec. Feel free to fork if you need something older! Targeted ActiveRecord
|
29
|
+
(Rails not required) compatibility follows the same scheme
|
30
|
+
as [Rails Security Issue maintenance policy](https://guides.rubyonrails.org/maintenance_policy.html#security-issues),
|
31
|
+
currently 6.1, 6.0, 5.2, but it is highly likely that this code will work in any version of ActiveRecord/Rails that runs
|
32
|
+
on Ruby 2+.
|
33
|
+
|
8
34
|
## Installation
|
9
35
|
|
10
36
|
Add this line to your application's Gemfile:
|
11
37
|
|
12
38
|
```ruby
|
13
|
-
gem
|
39
|
+
gem "shiftable"
|
14
40
|
```
|
15
41
|
|
16
42
|
And then execute:
|
@@ -61,32 +87,86 @@ But how can you accomplish this? If you used the `shiftable` gem, won't take but
|
|
61
87
|
|
62
88
|
class Spaceship < ActiveRecord::Base
|
63
89
|
belongs_to :captain
|
64
|
-
extend Shiftable::Single.new
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
90
|
+
extend Shiftable::Single.new belongs_to: :captain, has_one: :spaceship, precheck: true,
|
91
|
+
before_shift: ->(shifting:, shift_to:, shift_from:) { shifting.ownership_changes += 1 }
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
NOTE: It doesn't matter if the extend occurs before or after the association macro `belongs_to`. In fact, it doesn't matter so much that you can even do this...
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
|
99
|
+
class Spaceship < ActiveRecord::Base
|
100
|
+
belongs_to :captain
|
101
|
+
|
102
|
+
class << self
|
103
|
+
include Shiftable::Single.new belongs_to: :captain, has_one: :spaceship, precheck: true,
|
104
|
+
before_shift: lambda { |shifting:, shift_to:, shift_from:|
|
105
|
+
shifting.ownership_changes += 1
|
106
|
+
}
|
107
|
+
end
|
70
108
|
end
|
71
109
|
```
|
72
110
|
|
111
|
+
### Single Table Inheritance
|
112
|
+
|
113
|
+
This works as you would expect with STI (single table inheritance) classes, i.e. when defined on a subclass, only the records of that class get shifted.
|
114
|
+
|
73
115
|
### Multiple association on a single class
|
74
116
|
|
75
|
-
What if the
|
76
|
-
federation!
|
117
|
+
What if the captain and the spaceship have a boss... the space
|
118
|
+
federation! And in a run-in with their arch-Nemesis the Plinth-inth,
|
119
|
+
all federation spaceships are commandeered! You are ruined!
|
77
120
|
|
78
121
|
```ruby
|
79
122
|
|
123
|
+
class Spaceship < ActiveRecord::Base
|
124
|
+
belongs_to :space_federation
|
125
|
+
extend Shiftable::Collection.new belongs_to: :space_federation, has_one: :spaceship,
|
126
|
+
before_shift: lambda do |shifting_rel:, shift_to:, shift_from:|
|
127
|
+
shifting_rel.each { |spaceship| spaceship.federation_changes += 1 }
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class SpaceFederation < ActiveRecord::Base
|
132
|
+
has_many :spaceships
|
133
|
+
|
134
|
+
def all_spaceships_commandeered_by(nemesis_federation)
|
135
|
+
Spaceship.shift_cx(shift_to: nemesis_federation, shift_from: self)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
### Complete example
|
141
|
+
|
142
|
+
Putting it all together...
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
class Captain < ActiveRecord::Base
|
146
|
+
has_one :spaceship
|
147
|
+
|
148
|
+
def sell_spaceship_to(nemesis_captain)
|
149
|
+
Spaceship.shift_single(shift_to: nemesis_captain, shift_from: self)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
80
153
|
class Spaceship < ActiveRecord::Base
|
81
154
|
belongs_to :captain
|
155
|
+
extend Shiftable::Single.new belongs_to: :captain, has_one: :spaceship, precheck: true,
|
156
|
+
before_shift: ->(shifting:, shift_to:, shift_from:) { shifting.ownership_changes += 1 }
|
157
|
+
|
82
158
|
belongs_to :space_federation
|
159
|
+
extend Shiftable::Collection.new belongs_to: :space_federation, has_one: :spaceship,
|
160
|
+
before_shift: lambda do |shifting_rel:, shift_to:, shift_from:|
|
161
|
+
shifting_rel.each { |spaceship| spaceship.federation_changes += 1 }
|
162
|
+
end
|
83
163
|
end
|
84
164
|
|
85
165
|
class SpaceFederation < ActiveRecord::Base
|
86
166
|
has_many :captains
|
87
167
|
has_many :spaceships
|
88
168
|
|
89
|
-
def
|
169
|
+
def all_spaceships_commandeered_by(nemesis_federation)
|
90
170
|
Spaceship.shift_cx(shift_to: nemesis_federation, shift_from: self)
|
91
171
|
end
|
92
172
|
end
|
@@ -105,12 +185,63 @@ push git commits and the created tag, and push the `.gem` file to [rubygems.org]
|
|
105
185
|
|
106
186
|
## Contributing
|
107
187
|
|
108
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/pboling/shiftable. This project is intended to
|
188
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/pboling/shiftable. This project is intended to
|
189
|
+
be a safe, welcoming space for collaboration, and contributors are expected to adhere to
|
190
|
+
the [code of conduct](https://github.com/pboling/shiftable/blob/master/CODE_OF_CONDUCT.md).
|
191
|
+
|
192
|
+
## Code of Conduct
|
193
|
+
|
194
|
+
Everyone interacting in the Shiftable project's codebases, issue trackers, chat rooms and mailing lists is expected to
|
195
|
+
follow the [code of conduct](https://github.com/pboling/shiftable/blob/master/CODE_OF_CONDUCT.md).
|
196
|
+
|
197
|
+
## Versioning
|
198
|
+
|
199
|
+
This library aims to adhere to [Semantic Versioning 2.0.0][semver]. Violations of this scheme should be reported as
|
200
|
+
bugs. Specifically, if a minor or patch version is released that breaks backward compatibility, a new version should be
|
201
|
+
immediately released that restores compatibility. Breaking changes to the public API will only be introduced with new
|
202
|
+
major versions.
|
203
|
+
|
204
|
+
As a result of this policy, you can (and should) specify a dependency on this gem using
|
205
|
+
the [Pessimistic Version Constraint][pvc] with two digits of precision.
|
206
|
+
|
207
|
+
For example:
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
spec.add_dependency "shiftable", "~> 0.2"
|
211
|
+
```
|
109
212
|
|
110
213
|
## License
|
111
214
|
|
112
|
-
The gem is available as open source under the terms of
|
215
|
+
The gem is available as open source under the terms of
|
216
|
+
the [MIT License](https://opensource.org/licenses/MIT) [](https://opensource.org/licenses/MIT)
|
217
|
+
.
|
113
218
|
|
114
|
-
|
219
|
+
* Copyright (c) 2021 [Peter H. Boling][peterboling] of [Rails Bling][railsbling]
|
220
|
+
|
221
|
+
[license]: LICENSE
|
222
|
+
|
223
|
+
[semver]: http://semver.org/
|
224
|
+
|
225
|
+
[pvc]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint
|
226
|
+
|
227
|
+
[railsbling]: http://www.railsbling.com
|
228
|
+
|
229
|
+
[peterboling]: http://www.peterboling.com
|
230
|
+
|
231
|
+
[aboutme]: https://about.me/peter.boling
|
232
|
+
|
233
|
+
[angelme]: https://angel.co/peter-boling
|
234
|
+
|
235
|
+
[coderme]:http://coderwall.com/pboling
|
236
|
+
|
237
|
+
[followme-img]: https://img.shields.io/twitter/follow/galtzo.svg?style=social&label=Follow
|
238
|
+
|
239
|
+
[tweetme]: http://twitter.com/galtzo
|
240
|
+
|
241
|
+
[politicme]: https://nationalprogressiveparty.org
|
242
|
+
|
243
|
+
[documentation]: http://rdoc.info/github/pboling/shiftable/frames
|
244
|
+
|
245
|
+
[homepage]: https://github.com/pboling/shiftable/
|
115
246
|
|
116
|
-
|
247
|
+
[blogpage]: http://www.railsbling.com/tags/shiftable/
|
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,74 +14,60 @@
|
|
15
14
|
module Shiftable
|
16
15
|
# Inheriting from Module is a powerful pattern. If you like it checkout the debug_logging gem!
|
17
16
|
class Collection < Module
|
18
|
-
|
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) if before_save
|
82
|
-
shifting_rel.each do |shifting|
|
83
|
-
shifting.save
|
84
|
-
end
|
85
|
-
shifting_rel
|
69
|
+
define_method(:"#{prefix}shift_cx") do |shift_to:, shift_from:|
|
70
|
+
signature.shift_data!(shift_to: shift_to, shift_from: shift_from)
|
86
71
|
end
|
87
72
|
end
|
88
73
|
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shiftable
|
4
|
+
class ModSignature
|
5
|
+
VALID_TYPES = %i[sg cx].freeze
|
6
|
+
VALID_ASSOCIATIONS = {
|
7
|
+
sg: %i[belongs_to has_one],
|
8
|
+
cx: %i[belongs_to has_many]
|
9
|
+
}.freeze
|
10
|
+
DEFAULT_BEFORE_SHIFT = ->(*_) { true }
|
11
|
+
attr_reader :associations, :options, :type, :base
|
12
|
+
|
13
|
+
# Imagine you are a Spaceship Captain, the Spaceship belongs_to you, and it has only one Captain.
|
14
|
+
# But you have to sell it to your nemesis!
|
15
|
+
def initialize(associations:, type:, options: {})
|
16
|
+
@associations = associations
|
17
|
+
@options = options
|
18
|
+
@type = type
|
19
|
+
# See: https://maximomussini.com/posts/practical-applications-of-the-singleton-class/
|
20
|
+
singleton_class.send(:prepend, Object.const_get("Shiftable::ModSignature::#{type.capitalize}Methods", false))
|
21
|
+
validate
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate
|
25
|
+
raise ArgumentError, "type must be one of: #{VALID_TYPES}, provided: #{type}" if invalid_type?
|
26
|
+
raise ArgumentError, "associations must be symbols" if invalid_association_key_type?
|
27
|
+
raise ArgumentError, "exactly two distinct associations must be provided" if invalid_number_of_associations?
|
28
|
+
end
|
29
|
+
|
30
|
+
def invalid_type?
|
31
|
+
!VALID_TYPES.include?(type)
|
32
|
+
end
|
33
|
+
|
34
|
+
def invalid_association_key_type?
|
35
|
+
associations.keys.detect { |key| !key.is_a?(Symbol) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def invalid_number_of_associations?
|
39
|
+
associations.keys.uniq.length != 2
|
40
|
+
end
|
41
|
+
|
42
|
+
# @note Chainable
|
43
|
+
# @return self
|
44
|
+
def add_base(base)
|
45
|
+
@base = base
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def has_rel_name
|
50
|
+
VALID_ASSOCIATIONS[type][1]
|
51
|
+
end
|
52
|
+
|
53
|
+
def validate_relationships
|
54
|
+
bt = base.reflect_on_association(belongs_to)
|
55
|
+
raise ArgumentError, "Unable to find belongs_to: :#{belongs_to} in #{base}" unless bt
|
56
|
+
|
57
|
+
klass = bt.klass
|
58
|
+
hr = klass.reflect_on_association(has_rel)
|
59
|
+
raise ArgumentError, "Unable to find #{has_rel_name}: :#{has_rel} in #{klass}" unless hr
|
60
|
+
end
|
61
|
+
|
62
|
+
module CxMethods
|
63
|
+
def has_many
|
64
|
+
associations[:has_many]
|
65
|
+
end
|
66
|
+
|
67
|
+
alias has_rel has_many
|
68
|
+
|
69
|
+
def shift_data!(shift_to:, shift_from:)
|
70
|
+
validate_relationships
|
71
|
+
shifting_rel = ShiftingRelation.new(
|
72
|
+
to: shift_to,
|
73
|
+
from: shift_from,
|
74
|
+
column: shift_column,
|
75
|
+
base: base
|
76
|
+
)
|
77
|
+
shifting_rel.shift do |result|
|
78
|
+
before_shift&.call(shifting_rel: result, shift_to: shift_to, shift_from: shift_from)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
module SgMethods
|
84
|
+
def has_one
|
85
|
+
associations[:has_one]
|
86
|
+
end
|
87
|
+
|
88
|
+
alias has_rel has_one
|
89
|
+
|
90
|
+
# Do not move record if a record already exists (we are shifting a "has_one" association, after all)
|
91
|
+
def precheck
|
92
|
+
options[:precheck]
|
93
|
+
end
|
94
|
+
|
95
|
+
def shift_data!(shift_to:, shift_from:)
|
96
|
+
validate_relationships
|
97
|
+
shifting = ShiftingRecord.new(
|
98
|
+
to: shift_to,
|
99
|
+
from: shift_from,
|
100
|
+
column: shift_column,
|
101
|
+
base: base
|
102
|
+
) do
|
103
|
+
!precheck || !shift_to.send(has_one)
|
104
|
+
end
|
105
|
+
shifting.shift do |result|
|
106
|
+
before_shift&.call(shifting: result, shift_to: shift_to, shift_from: shift_from)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# The name of the belongs_to association, defined on the shifting model, e.g. Spaceship
|
112
|
+
# Normally a camel-cased, symbolized, version of the class name.
|
113
|
+
# In the case where Spaceship belongs_to: :captain, this is :captain.
|
114
|
+
def belongs_to
|
115
|
+
associations[:belongs_to]
|
116
|
+
end
|
117
|
+
|
118
|
+
def method_prefix
|
119
|
+
options[:method_prefix]
|
120
|
+
end
|
121
|
+
|
122
|
+
# will prevent the save if it returns false
|
123
|
+
# allows for any custom logic to be run, such as setting shift_from attributes, prior to the shift is saved.
|
124
|
+
def before_shift
|
125
|
+
options[:before_shift] || DEFAULT_BEFORE_SHIFT
|
126
|
+
end
|
127
|
+
|
128
|
+
def shift_column
|
129
|
+
reflection = base.reflect_on_association(belongs_to).klass.reflect_on_association(has_rel)
|
130
|
+
reflection.foreign_key
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shiftable
|
4
|
+
# Gets data to be shifted
|
5
|
+
class Shifting
|
6
|
+
attr_reader :to, :from, :column, :base, :result, :run_save
|
7
|
+
|
8
|
+
def initialize(to:, from:, column:, base:)
|
9
|
+
@to = to
|
10
|
+
@from = from
|
11
|
+
@column = column
|
12
|
+
@base = base
|
13
|
+
validate
|
14
|
+
do_query = block_given? ? yield : true
|
15
|
+
@result = do_query ? query : nil
|
16
|
+
@run_save = true
|
17
|
+
end
|
18
|
+
|
19
|
+
# def found?
|
20
|
+
# raise "found? must be defined in a subclass"
|
21
|
+
# end
|
22
|
+
|
23
|
+
# def shift
|
24
|
+
# raise "shift must be defined in a subclass"
|
25
|
+
# end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def validate
|
30
|
+
raise ArgumentError, "shift_to must have an id (primary key) value, but is: #{to&.id}" unless to&.id
|
31
|
+
raise ArgumentError, "shift_from must have an id (primary key) value, but is: #{from&.id}" unless from&.id
|
32
|
+
end
|
33
|
+
|
34
|
+
# def query
|
35
|
+
# raise "query must be defined in a subclass"
|
36
|
+
# end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shiftable
|
4
|
+
# Gets data to be shifted
|
5
|
+
class ShiftingRecord < Shifting
|
6
|
+
def found?
|
7
|
+
!!result
|
8
|
+
end
|
9
|
+
|
10
|
+
# @return true, false
|
11
|
+
def shift
|
12
|
+
return false unless found?
|
13
|
+
|
14
|
+
result.send("#{column}=", to.id)
|
15
|
+
@run_save = yield result if block_given?
|
16
|
+
result.save if run_save
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def query
|
22
|
+
base.find_by(column => from.id)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shiftable
|
4
|
+
# Gets data to be shifted
|
5
|
+
class ShiftingRelation < Shifting
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def found?
|
9
|
+
result.any?
|
10
|
+
end
|
11
|
+
|
12
|
+
def each(&block)
|
13
|
+
result.each(&block)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return result (once it is shifted)
|
17
|
+
def shift
|
18
|
+
return false unless found?
|
19
|
+
|
20
|
+
each do |record|
|
21
|
+
record.send("#{column}=", to.id)
|
22
|
+
end
|
23
|
+
@run_save = yield result if block_given?
|
24
|
+
each(&:save) if run_save
|
25
|
+
result
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def query
|
31
|
+
base.where(column => from.id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|