shiftable 0.4.1 → 0.5.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 +22 -5
- data/CODE_OF_CONDUCT.md +0 -0
- data/CONTRIBUTING.md +0 -0
- data/LICENSE.txt +0 -0
- data/README.md +101 -3
- data/lib/shiftable/collection.rb +7 -5
- data/lib/shiftable/mod_signature.rb +52 -15
- data/lib/shiftable/shifting.rb +0 -0
- data/lib/shiftable/shifting_polymorphic_relation.rb +45 -0
- data/lib/shiftable/shifting_record.rb +0 -0
- data/lib/shiftable/shifting_relation.rb +0 -0
- data/lib/shiftable/single.rb +1 -1
- data/lib/shiftable/version.rb +1 -1
- data/lib/shiftable.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 678fdd1fc65199227c201ecd4346e4c0019c78747d949ac9b4dcba8b38158c24
|
4
|
+
data.tar.gz: 3f527ed0b2e70a2e677b006320b3e667d0512ccfcacc5bb76232129b9f37c6d0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c281d086eabfbce50dcd9f3098374656a8c7536b0ce1d5e21d5262c6e205172f90930aa7120a747904d6599313c235adba0bc5487011fa7b5df0e8c90af9157b
|
7
|
+
data.tar.gz: 217eb1fb303dbc45ccb962bddbff8c4dc246b20351b1f1f2312b9e4ea8c4db7a90360c089b040f0276e544a38193d01feb1728bd6f3949dfe849509a12d38a64
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,28 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
-
|
3
|
+
### Added
|
4
|
+
|
5
|
+
|
6
|
+
### Changed
|
7
|
+
|
8
|
+
|
9
|
+
### Fixed
|
10
|
+
|
11
|
+
|
12
|
+
### Removed
|
13
|
+
|
4
14
|
|
15
|
+
## [0.5.0] - 2021-11-12
|
16
|
+
### Added
|
17
|
+
|
18
|
+
- Support for Polymorphic associations (see examples in specs or README)
|
19
|
+
|
20
|
+
## [0.4.1] - 2021-11-10
|
21
|
+
### Fixed
|
22
|
+
|
23
|
+
- Documentation typos in README
|
24
|
+
|
25
|
+
## [0.4.0] - 2021-10-27
|
5
26
|
### Changed
|
6
27
|
|
7
28
|
- option :preflight_checks renamed to :precheck
|
@@ -11,7 +32,6 @@
|
|
11
32
|
- Even more 100% spec coverage
|
12
33
|
|
13
34
|
## [0.3.0] - 2021-10-26
|
14
|
-
|
15
35
|
### Changed
|
16
36
|
|
17
37
|
- Internal rewrite to improve maintainability
|
@@ -22,7 +42,6 @@
|
|
22
42
|
- Even more 100% spec coverage
|
23
43
|
|
24
44
|
## [0.2.0] - 2021-10-24
|
25
|
-
|
26
45
|
### Changed
|
27
46
|
|
28
47
|
- option `before_save` is now `before_shift` as originally documented
|
@@ -34,7 +53,6 @@
|
|
34
53
|
- Documentation
|
35
54
|
|
36
55
|
## [0.1.1] - 2021-10-23
|
37
|
-
|
38
56
|
### Fixed
|
39
57
|
|
40
58
|
- Github Actions build
|
@@ -44,7 +62,6 @@
|
|
44
62
|
- Linting
|
45
63
|
|
46
64
|
## [0.1.0] - 2021-10-23
|
47
|
-
|
48
65
|
### Added
|
49
66
|
|
50
67
|
- Initial release
|
data/CODE_OF_CONDUCT.md
CHANGED
File without changes
|
data/CONTRIBUTING.md
CHANGED
File without changes
|
data/LICENSE.txt
CHANGED
File without changes
|
data/README.md
CHANGED
@@ -104,7 +104,7 @@ end
|
|
104
104
|
|
105
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
106
|
|
107
|
-
### Multiple
|
107
|
+
### Multiple associations on a single class
|
108
108
|
|
109
109
|
What if the captain and the spaceship have a boss... the space
|
110
110
|
federation! And in a run-in with their arch-Nemesis the Plinth-inth,
|
@@ -114,7 +114,7 @@ all federation spaceships are commandeered! You are ruined!
|
|
114
114
|
|
115
115
|
class Spaceship < ActiveRecord::Base
|
116
116
|
belongs_to :space_federation
|
117
|
-
extend Shiftable::Collection.new belongs_to: :space_federation,
|
117
|
+
extend Shiftable::Collection.new belongs_to: :space_federation, has_many: :spaceships,
|
118
118
|
before_shift: lambda { |shifting_rel:, shift_to:, shift_from:|
|
119
119
|
shifting_rel.each { |spaceship| spaceship.federation_changes += 1 }
|
120
120
|
}
|
@@ -129,6 +129,68 @@ class SpaceFederation < ActiveRecord::Base
|
|
129
129
|
end
|
130
130
|
```
|
131
131
|
|
132
|
+
### Polymorphism and has_many through
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
class SpaceTreaty < ActiveRecord::Base
|
136
|
+
has_many :space_treaty_signature
|
137
|
+
end
|
138
|
+
|
139
|
+
class SpaceTreatySignature < ActiveRecord::Base
|
140
|
+
belongs_to :space_treaty
|
141
|
+
belongs_to :signatory, polymorphic: true
|
142
|
+
# When two space federations assimilate (i.e. merge) to form a single larger federation,
|
143
|
+
# they become party to (i.e. signatories of) all the treaties that had been signed by either.
|
144
|
+
# In practical terms, this means:
|
145
|
+
#
|
146
|
+
# surviving_federation = SpaceFederation.find(1)
|
147
|
+
# assimilated_federation = SpaceFederation.find(2)
|
148
|
+
# SpaceTreatySignature.where(
|
149
|
+
# signatory_id: assimilated_federation_id,
|
150
|
+
# signatory_type: "SpaceFederation"
|
151
|
+
# ).update_all(
|
152
|
+
# signatory_id: surviving_federation.id
|
153
|
+
# )
|
154
|
+
extend Shiftable::Collection.new(
|
155
|
+
belongs_to: :signatory, has_many: :space_treaty_signature,
|
156
|
+
polymorphic_type: "SpaceFederation",
|
157
|
+
method_prefix: "space_federation_",
|
158
|
+
before_shift: lambda { |shifting_rel:, shift_to:, shift_from:|
|
159
|
+
# Each item in shifting_rel is an instance of the class where Shiftable::Collection is defined,
|
160
|
+
# in this case: SpaceTreatySignature
|
161
|
+
# And each of them has a signatory which is of type "SpaceFederation",
|
162
|
+
# because a polymorphic collection only targets one type.
|
163
|
+
# shifting_rel.each { |signature| signature.signatory == "SpaceFederation" }
|
164
|
+
}
|
165
|
+
)
|
166
|
+
end
|
167
|
+
|
168
|
+
class SpaceFederation < ActiveRecord::Base
|
169
|
+
has_many :space_treaty_signature, as: :signatory
|
170
|
+
has_many :space_treaties, through: :space_treaty_signatures, as: :signatory
|
171
|
+
has_many :treaty_planets, class_name: "Planet", through: :space_treaty_signatures, as: :signatory
|
172
|
+
has_many :treaty_stations, class_name: "SpaceStation", through: :space_treaty_signatures, as: :signatory
|
173
|
+
def assimilate_from(other_federation)
|
174
|
+
SpaceTreatySignature.space_federation_shift_cx(shift_to: self, shift_from: other_federation)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Including Planet and SpaceStation, for completeness of the example as the other "types" of polymorphic signatories
|
179
|
+
class Planet < ActiveRecord::Base
|
180
|
+
has_many :space_treaty_signature, as: :signatory
|
181
|
+
has_many :space_treaties, through: :space_treaty_signatures
|
182
|
+
has_many :treaty_federations, class_name: "SpaceFederation", through: :space_treaty_signatures, as: :signatory
|
183
|
+
has_many :treaty_stations, class_name: "SpaceStation", through: :space_treaty_signatures, as: :signatory
|
184
|
+
end
|
185
|
+
|
186
|
+
class SpaceStation < ActiveRecord::Base
|
187
|
+
has_many :space_treaty_signature, as: :signatory
|
188
|
+
has_many :space_treaties, through: :space_treaty_signatures
|
189
|
+
has_many :treaty_federations, class_name: "SpaceFederation", through: :space_treaty_signatures, as: :signatory
|
190
|
+
has_many :treaty_planets, class_name: "Planet", through: :space_treaty_signatures, as: :signatory
|
191
|
+
end
|
192
|
+
```#<--rubocop/md-->#<--rubocop/md-->`
|
193
|
+
|
132
194
|
### Complete example
|
133
195
|
|
134
196
|
Putting it all together...
|
@@ -148,7 +210,7 @@ class Spaceship < ActiveRecord::Base
|
|
148
210
|
before_shift: ->(shifting:, shift_to:, shift_from:) { shifting.ownership_changes += 1 }
|
149
211
|
|
150
212
|
belongs_to :space_federation
|
151
|
-
extend Shiftable::Collection.new belongs_to: :space_federation,
|
213
|
+
extend Shiftable::Collection.new belongs_to: :space_federation, has_many: :spaceships,
|
152
214
|
before_shift: lambda { |shifting_rel:, shift_to:, shift_from:|
|
153
215
|
shifting_rel.each { |spaceship| spaceship.federation_changes += 1 }
|
154
216
|
}
|
@@ -157,11 +219,47 @@ end
|
|
157
219
|
class SpaceFederation < ActiveRecord::Base
|
158
220
|
has_many :captains
|
159
221
|
has_many :spaceships
|
222
|
+
has_many :space_treaty_signature, as: :signatory
|
223
|
+
has_many :space_treaties, through: :space_treaty_signatures, as: :signatory
|
224
|
+
has_many :treaty_planets, class_name: "Planet", through: :space_treaty_signatures, as: :signatory
|
225
|
+
has_many :treaty_stations, class_name: "SpaceStation", through: :space_treaty_signatures, as: :signatory
|
226
|
+
|
227
|
+
def assimilate_from(other_federation)
|
228
|
+
SpaceTreatySignature.space_federation_shift_cx(shift_to: self, shift_from: other_federation)
|
229
|
+
end
|
160
230
|
|
161
231
|
def all_spaceships_commandeered_by(nemesis_federation)
|
162
232
|
Spaceship.shift_cx(shift_to: nemesis_federation, shift_from: self)
|
163
233
|
end
|
164
234
|
end
|
235
|
+
class SpaceTreaty < ActiveRecord::Base
|
236
|
+
has_many :space_treaty_signatures
|
237
|
+
end
|
238
|
+
|
239
|
+
class SpaceTreatySignature < ActiveRecord::Base
|
240
|
+
belongs_to :space_treaty
|
241
|
+
belongs_to :signatory, polymorphic: true
|
242
|
+
extend Shiftable::Collection.new(
|
243
|
+
belongs_to: :signatory, has_many: :space_treaty_signatures,
|
244
|
+
polymorphic_type: "SpaceFederation",
|
245
|
+
method_prefix: "space_federation_"
|
246
|
+
)
|
247
|
+
end
|
248
|
+
|
249
|
+
# Including Planet and SpaceStation, for completeness of the example as the other "types" of polymorphic signatories
|
250
|
+
class Planet < ActiveRecord::Base
|
251
|
+
has_many :space_treaty_signatures, as: :signatory
|
252
|
+
has_many :space_treaties, through: :space_treaty_signatures
|
253
|
+
has_many :treaty_federations, class_name: "SpaceFederation", through: :space_treaty_signatures, as: :signatory
|
254
|
+
has_many :treaty_stations, class_name: "SpaceStation", through: :space_treaty_signatures, as: :signatory
|
255
|
+
end
|
256
|
+
|
257
|
+
class SpaceStation < ActiveRecord::Base
|
258
|
+
has_many :space_treaty_signature, as: :signatory
|
259
|
+
has_many :space_treaties, through: :space_treaty_signatures
|
260
|
+
has_many :treaty_federations, class_name: "SpaceFederation", through: :space_treaty_signatures, as: :signatory
|
261
|
+
has_many :treaty_planets, class_name: "Planet", through: :space_treaty_signatures, as: :signatory
|
262
|
+
end
|
165
263
|
```
|
166
264
|
|
167
265
|
... stay tuned!
|
data/lib/shiftable/collection.rb
CHANGED
@@ -16,7 +16,7 @@ module Shiftable
|
|
16
16
|
class Collection < Module
|
17
17
|
# associations: belongs_to, has_many
|
18
18
|
# options: method_prefix, before_shift
|
19
|
-
def initialize(belongs_to:, has_many:, method_prefix: nil, before_shift: nil)
|
19
|
+
def initialize(belongs_to:, has_many:, polymorphic: nil, method_prefix: nil, before_shift: nil)
|
20
20
|
# Ruby's Module initializer doesn't take any arguments
|
21
21
|
super()
|
22
22
|
|
@@ -35,12 +35,13 @@ module Shiftable
|
|
35
35
|
has_many: has_many.to_s.to_sym
|
36
36
|
},
|
37
37
|
options: {
|
38
|
+
polymorphic: polymorphic,
|
38
39
|
method_prefix: method_prefix,
|
39
40
|
# will prevent the save if it returns false
|
40
41
|
# allows for any custom logic to be run, such as setting attributes, prior to the shift (save).
|
41
42
|
before_shift: before_shift
|
42
43
|
},
|
43
|
-
type: :cx
|
44
|
+
type: polymorphic ? :pcx : :cx
|
44
45
|
)
|
45
46
|
end
|
46
47
|
|
@@ -62,11 +63,12 @@ module Shiftable
|
|
62
63
|
module ShiftCollectionModulizer
|
63
64
|
def to_mod(signature)
|
64
65
|
prefix = signature.method_prefix
|
66
|
+
type = signature.type
|
65
67
|
Module.new do
|
66
|
-
define_method(:"#{prefix}
|
67
|
-
signature.
|
68
|
+
define_method(:"#{prefix}shift_#{type}_column") do
|
69
|
+
signature.send("shift_#{type}_column")
|
68
70
|
end
|
69
|
-
define_method(:"#{prefix}
|
71
|
+
define_method(:"#{prefix}shift_#{type}") do |shift_to:, shift_from:|
|
70
72
|
signature.shift_data!(shift_to: shift_to, shift_from: shift_from)
|
71
73
|
end
|
72
74
|
end
|
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
module Shiftable
|
4
4
|
class ModSignature
|
5
|
-
|
6
|
-
|
5
|
+
VALID_ASSOCIATIONS = { sg: %i[belongs_to has_one], cx: %i[belongs_to has_many], pcx: %i[belongs_to has_many] }.freeze
|
6
|
+
VALID_TYPES = VALID_ASSOCIATIONS.keys.dup.freeze
|
7
7
|
DEFAULT_BEFORE_SHIFT = ->(*_) { true }
|
8
8
|
attr_reader :associations, :options, :type, :base
|
9
9
|
|
@@ -24,6 +24,14 @@ module Shiftable
|
|
24
24
|
raise ArgumentError, "exactly two distinct associations must be provided" if invalid_number_of_associations?
|
25
25
|
end
|
26
26
|
|
27
|
+
def polymorphic_type
|
28
|
+
options.dig(:polymorphic, :type)
|
29
|
+
end
|
30
|
+
|
31
|
+
def polymorphic_as
|
32
|
+
options.dig(:polymorphic, :as)
|
33
|
+
end
|
34
|
+
|
27
35
|
def invalid_type?
|
28
36
|
!VALID_TYPES.include?(type)
|
29
37
|
end
|
@@ -48,21 +56,21 @@ module Shiftable
|
|
48
56
|
end
|
49
57
|
|
50
58
|
def validate_relationships
|
51
|
-
|
52
|
-
raise ArgumentError, "Unable to find belongs_to: :#{belongs_to} in #{base}" unless
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
59
|
+
bt_reflection = base.reflect_on_association(belongs_to)
|
60
|
+
raise ArgumentError, "Unable to find belongs_to: :#{belongs_to} in #{base}" unless bt_reflection
|
61
|
+
# We can't validate any further if the reflection is polymorphic
|
62
|
+
return true if bt_reflection.polymorphic?
|
63
|
+
|
64
|
+
klass = bt_reflection.klass
|
65
|
+
has_reflection = klass.reflect_on_association(has_rel)
|
66
|
+
raise ArgumentError, "Unable to find #{has_rel_name}: :#{has_rel} in #{klass}" unless has_reflection
|
57
67
|
end
|
58
68
|
|
59
69
|
module CxMethods
|
60
|
-
def
|
70
|
+
def has_rel
|
61
71
|
associations[:has_many]
|
62
72
|
end
|
63
73
|
|
64
|
-
alias has_rel has_many
|
65
|
-
|
66
74
|
def shift_data!(shift_to:, shift_from:)
|
67
75
|
validate_relationships
|
68
76
|
shifting_rel = ShiftingRelation.new(
|
@@ -77,13 +85,35 @@ module Shiftable
|
|
77
85
|
end
|
78
86
|
end
|
79
87
|
|
88
|
+
module PcxMethods
|
89
|
+
# This method could be defined for parity, but it is never used.
|
90
|
+
# def has_rel
|
91
|
+
# associations[:has_many]
|
92
|
+
# end
|
93
|
+
|
94
|
+
def shift_data!(shift_to:, shift_from:)
|
95
|
+
validate_relationships
|
96
|
+
shifting_rel = ShiftingPolymorphicRelation.new(
|
97
|
+
to: shift_to,
|
98
|
+
from: shift_from,
|
99
|
+
column: {
|
100
|
+
type: polymorphic_type,
|
101
|
+
as: polymorphic_as,
|
102
|
+
id_column: shift_pcx_column
|
103
|
+
},
|
104
|
+
base: base
|
105
|
+
)
|
106
|
+
shifting_rel.shift do |result|
|
107
|
+
before_shift&.call(shifting_rel: result, shift_to: shift_to, shift_from: shift_from)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
80
112
|
module SgMethods
|
81
|
-
def
|
113
|
+
def has_rel
|
82
114
|
associations[:has_one]
|
83
115
|
end
|
84
116
|
|
85
|
-
alias has_rel has_one
|
86
|
-
|
87
117
|
# Do not move record if a record already exists (we are shifting a "has_one" association, after all)
|
88
118
|
def precheck
|
89
119
|
options[:precheck]
|
@@ -97,7 +127,7 @@ module Shiftable
|
|
97
127
|
column: shift_column,
|
98
128
|
base: base
|
99
129
|
) do
|
100
|
-
!precheck || !shift_to.send(
|
130
|
+
!precheck || !shift_to.send(has_rel)
|
101
131
|
end
|
102
132
|
shifting.shift do |result|
|
103
133
|
before_shift&.call(shifting: result, shift_to: shift_to, shift_from: shift_from)
|
@@ -122,9 +152,16 @@ module Shiftable
|
|
122
152
|
options[:before_shift] || DEFAULT_BEFORE_SHIFT
|
123
153
|
end
|
124
154
|
|
155
|
+
def shift_pcx_column
|
156
|
+
"#{polymorphic_as}_id"
|
157
|
+
end
|
158
|
+
|
125
159
|
def shift_column
|
126
160
|
reflection = base.reflect_on_association(belongs_to).klass.reflect_on_association(has_rel)
|
127
161
|
reflection.foreign_key
|
128
162
|
end
|
163
|
+
|
164
|
+
alias shift_sg_column shift_column
|
165
|
+
alias shift_cx_column shift_column
|
129
166
|
end
|
130
167
|
end
|
data/lib/shiftable/shifting.rb
CHANGED
File without changes
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shiftable
|
4
|
+
# Gets data to be shifted
|
5
|
+
class ShiftingPolymorphicRelation < Shifting
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def polymorphic_id_column
|
9
|
+
column[:id_column]
|
10
|
+
end
|
11
|
+
|
12
|
+
def polymorphic_type_column
|
13
|
+
"#{column[:as]}_type"
|
14
|
+
end
|
15
|
+
|
16
|
+
def found?
|
17
|
+
result.any?
|
18
|
+
end
|
19
|
+
|
20
|
+
def each(&block)
|
21
|
+
result.each(&block)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return result (once it is shifted)
|
25
|
+
def shift
|
26
|
+
return false unless found?
|
27
|
+
|
28
|
+
each do |record|
|
29
|
+
record.send("#{polymorphic_id_column}=", to.id)
|
30
|
+
end
|
31
|
+
@run_save = yield result if block_given?
|
32
|
+
each(&:save) if run_save
|
33
|
+
result
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def query
|
39
|
+
base.where(
|
40
|
+
polymorphic_type_column => column[:type],
|
41
|
+
polymorphic_id_column => from.id
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
File without changes
|
File without changes
|
data/lib/shiftable/single.rb
CHANGED
@@ -70,7 +70,7 @@ module Shiftable
|
|
70
70
|
prefix = signature.method_prefix
|
71
71
|
Module.new do
|
72
72
|
define_method(:"#{prefix}shift_column") do
|
73
|
-
signature.
|
73
|
+
signature.send("shift_#{signature.type}_column")
|
74
74
|
end
|
75
75
|
define_method(:"#{prefix}shift_single") do |shift_to:, shift_from:|
|
76
76
|
signature.shift_data!(shift_to: shift_to, shift_from: shift_from)
|
data/lib/shiftable/version.rb
CHANGED
data/lib/shiftable.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require_relative "shiftable/version"
|
4
4
|
require_relative "shiftable/shifting"
|
5
5
|
require_relative "shiftable/shifting_record"
|
6
|
+
require_relative "shiftable/shifting_polymorphic_relation"
|
6
7
|
require_relative "shiftable/shifting_relation"
|
7
8
|
require_relative "shiftable/mod_signature"
|
8
9
|
require_relative "shiftable/collection"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shiftable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Boling
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-11-
|
11
|
+
date: 2021-11-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -181,6 +181,7 @@ files:
|
|
181
181
|
- lib/shiftable/collection.rb
|
182
182
|
- lib/shiftable/mod_signature.rb
|
183
183
|
- lib/shiftable/shifting.rb
|
184
|
+
- lib/shiftable/shifting_polymorphic_relation.rb
|
184
185
|
- lib/shiftable/shifting_record.rb
|
185
186
|
- lib/shiftable/shifting_relation.rb
|
186
187
|
- lib/shiftable/single.rb
|