shiftable 0.1.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) |
|
12
|
+
| download rank | [![Downloads Today](https://img.shields.io/gem/rd/shiftable.svg)](https://github.com/pboling/shiftable) |
|
13
|
+
| version | [![Version](https://img.shields.io/gem/v/shiftable.svg)](https://rubygems.org/gems/shiftable) |
|
14
|
+
| dependencies | [![Depfu](https://badges.depfu.com/badges/0412727b7e3b740b950a683eebc708e2/count.svg)](https://depfu.com/github/pboling/shiftable?project_id=32594) |
|
15
|
+
| unit tests | [![unit tests](https://github.com/pboling/shiftable/actions/workflows/test.yml/badge.svg)](https://github.com/pboling/shiftable/actions) |
|
16
|
+
| lint status | [![lint status](https://github.com/pboling/shiftable/actions/workflows/style.yml/badge.svg)](https://github.com/pboling/shiftable/actions) |
|
17
|
+
| unsupported status | [![unsupported status](https://github.com/pboling/shiftable/actions/workflows/unsupported.yml/badge.svg)](https://github.com/pboling/shiftable/actions) |
|
18
|
+
| test coverage | [![Test Coverage](https://api.codeclimate.com/v1/badges/a53aa8b7c413b950d519/test_coverage)](https://codeclimate.com/github/pboling/shiftable/test_coverage) |
|
19
|
+
| maintainability | [![Maintainability](https://api.codeclimate.com/v1/badges/a53aa8b7c413b950d519/maintainability)](https://codeclimate.com/github/pboling/shiftable/maintainability) |
|
20
|
+
| code triage | [![Open Source Helpers](https://www.codetriage.com/pboling/shiftable/badges/users.svg)](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) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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
|