shiftable 0.2.0 → 0.3.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 +11 -0
- data/lib/shiftable/collection.rb +37 -51
- data/lib/shiftable/mod_signature.rb +164 -0
- data/lib/shiftable/single.rb +35 -54
- data/lib/shiftable/version.rb +1 -1
- data/lib/shiftable.rb +2 -1
- 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: 15d27c7c2706faf797241101fa5229f4bf707b06f4d1b7c839cc66fd7ab2ecf8
|
4
|
+
data.tar.gz: 0fa0fbe15096d109c2d63568101ad0589b4c684775b69b95e831289b8bfdac3d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fa80c525bb2a0cfa99a82350da151412972f86a6699e1091e520d9cec40466f07789e5d1e593f8e3b720cc9e447d742acf28f2c09f5b02c61745cf8ca79084cf
|
7
|
+
data.tar.gz: 4abc0b5c410ba1748646122914ac768829984934e1f57b4d0483db18d0d5985b1976d69deeceabb3bf04fc5b4e3b6307056e3eeae2cffbd89e8099059cb32c07
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.3.0] - 2021-10-26
|
4
|
+
|
5
|
+
### Changed
|
6
|
+
|
7
|
+
- Internal rewrite to improve maintainability
|
8
|
+
- Extreme edge cases involving incorrect configuration will raise slightly different errors.
|
9
|
+
|
10
|
+
### Added
|
11
|
+
|
12
|
+
- Even more 100% spec coverage
|
13
|
+
|
3
14
|
## [0.2.0] - 2021-10-24
|
4
15
|
|
5
16
|
### Changed
|
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,59 @@
|
|
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_shift)
|
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_shift)
|
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)
|
58
64
|
Module.new do
|
59
|
-
define_method(:"#{mepr}shift_cx_column") do
|
60
|
-
|
61
|
-
reflection.foreign_key
|
65
|
+
define_method(:"#{signature.mepr}shift_cx_column") do
|
66
|
+
signature.shift_column
|
62
67
|
end
|
63
|
-
define_method(:"#{mepr}
|
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_shift&.call(shifting_rel: shifting_rel, shift_to: shift_to, shift_from: shift_from)
|
82
|
-
shifting_rel.each(&:save)
|
83
|
-
shifting_rel
|
68
|
+
define_method(:"#{signature.mepr}shift_cx") do |shift_to:, shift_from:|
|
69
|
+
signature.shift_data!(shift_to: shift_to, shift_from: shift_from)
|
84
70
|
end
|
85
71
|
end
|
86
72
|
end
|
@@ -0,0 +1,164 @@
|
|
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
|
+
attr_accessor :associations, :options, :type
|
11
|
+
attr_reader :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}" unless VALID_TYPES.include?(type)
|
26
|
+
raise ArgumentError, "associations must be symbols" if associations.keys.detect { |a| !a.is_a?(Symbol) }
|
27
|
+
raise ArgumentError, "exactly two distinct associations must be provided" unless associations.keys.uniq.length == 2
|
28
|
+
|
29
|
+
invalid_tokens = associations.keys - VALID_ASSOCIATIONS[type]
|
30
|
+
raise ArgumentError, "valid associations: #{VALID_ASSOCIATIONS[type]}, invalid: #{invalid_tokens}" if invalid_tokens.any?
|
31
|
+
end
|
32
|
+
|
33
|
+
# @note Chainable
|
34
|
+
# @return self
|
35
|
+
def add_base(base)
|
36
|
+
@base = base
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def has_rel_name
|
41
|
+
VALID_ASSOCIATIONS[type][1]
|
42
|
+
end
|
43
|
+
|
44
|
+
def validate_relationships
|
45
|
+
bt = base.reflect_on_association(belongs_to)
|
46
|
+
raise ArgumentError, "Unable to find belongs_to: :#{belongs_to} in #{base}" unless bt
|
47
|
+
|
48
|
+
hr = bt.klass.reflect_on_association(has_rel)
|
49
|
+
raise ArgumentError, "Unable to find #{has_rel_name}: :#{has_rel} in #{bt.klass}" unless hr
|
50
|
+
end
|
51
|
+
|
52
|
+
module CxMethods
|
53
|
+
def has_many
|
54
|
+
associations[:has_many]
|
55
|
+
end
|
56
|
+
|
57
|
+
alias has_rel has_many
|
58
|
+
|
59
|
+
# returns nil or ActiveRecord::Relation object
|
60
|
+
def data_for_shift(id)
|
61
|
+
base.where(send("shift_column") => id) if super
|
62
|
+
end
|
63
|
+
|
64
|
+
# returns false, nil or ActiveRecord::Relation object
|
65
|
+
def data_for_shift_safe(shift_to:, shift_from:)
|
66
|
+
return false unless super
|
67
|
+
|
68
|
+
data_for_shift(shift_from.id)
|
69
|
+
end
|
70
|
+
|
71
|
+
def shift_data!(shift_to:, shift_from:)
|
72
|
+
validate_relationships
|
73
|
+
|
74
|
+
shifting_rel = data_for_shift_safe(shift_to: shift_to, shift_from: shift_from)
|
75
|
+
return false unless shifting_rel && shifting_rel.any?
|
76
|
+
|
77
|
+
shifting_rel.each do |shifting|
|
78
|
+
shifting.send("#{send(:shift_column)}=", shift_to.id)
|
79
|
+
end
|
80
|
+
before_shift&.call(shifting_rel: shifting_rel, shift_to: shift_to, shift_from: shift_from)
|
81
|
+
shifting_rel.each(&:save)
|
82
|
+
shifting_rel
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
module SgMethods
|
87
|
+
def has_one
|
88
|
+
associations[:has_one]
|
89
|
+
end
|
90
|
+
|
91
|
+
alias has_rel has_one
|
92
|
+
|
93
|
+
# Do not move record if a record already exists (we are shifting a "has_one" association, after all)
|
94
|
+
def preflight_checks
|
95
|
+
options[:preflight_checks]
|
96
|
+
end
|
97
|
+
|
98
|
+
# returns nil or ActiveRecord object
|
99
|
+
def data_for_shift(id)
|
100
|
+
base.find_by(send("shift_column") => id) if super
|
101
|
+
end
|
102
|
+
|
103
|
+
# returns false, nil or ActiveRecord object
|
104
|
+
def data_for_shift_safe(shift_to:, shift_from:)
|
105
|
+
return false unless super
|
106
|
+
|
107
|
+
if preflight_checks
|
108
|
+
already_exists = shift_to.send(has_one)
|
109
|
+
return false if already_exists
|
110
|
+
end
|
111
|
+
data_for_shift(shift_from.id)
|
112
|
+
end
|
113
|
+
|
114
|
+
def shift_data!(shift_to:, shift_from:)
|
115
|
+
validate_relationships
|
116
|
+
|
117
|
+
shifting = data_for_shift_safe(shift_to: shift_to, shift_from: shift_from)
|
118
|
+
return false unless shifting
|
119
|
+
|
120
|
+
shifting.send("#{send(:shift_column)}=", shift_to.id)
|
121
|
+
shifting.save if before_shift.nil? || before_shift.call(shifting: shifting, shift_to: shift_to,
|
122
|
+
shift_from: shift_from)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# The name of the belongs_to association, defined on the shifting model, e.g. Spaceship
|
127
|
+
# Normally a camel-cased, symbolized, version of the class name.
|
128
|
+
# In the case where Spaceship belongs_to: :captain, this is :captain.
|
129
|
+
def belongs_to
|
130
|
+
associations[:belongs_to]
|
131
|
+
end
|
132
|
+
|
133
|
+
def method_prefix
|
134
|
+
options[:method_prefix]
|
135
|
+
end
|
136
|
+
|
137
|
+
alias mepr method_prefix
|
138
|
+
|
139
|
+
# will prevent the save if it returns false
|
140
|
+
# allows for any custom logic to be run, such as setting shift_from attributes, prior to the shift is saved.
|
141
|
+
def before_shift
|
142
|
+
options[:before_shift]
|
143
|
+
end
|
144
|
+
|
145
|
+
def shift_column
|
146
|
+
reflection = base.reflect_on_association(belongs_to).klass.reflect_on_association(has_rel)
|
147
|
+
reflection.foreign_key
|
148
|
+
end
|
149
|
+
|
150
|
+
# Effect is to short-circuit data_for_shift method prior to executing the ActiveRecord query
|
151
|
+
# if there is no ID, to avoid a full table scan when no id provided,
|
152
|
+
# e.g. where(id: nil).
|
153
|
+
def data_for_shift(id)
|
154
|
+
true if id
|
155
|
+
end
|
156
|
+
|
157
|
+
# Effect is to short-circuit data_for_shift_safe method prior to executing the ActiveRecord query
|
158
|
+
# if there is no ID, to avoid a full table scan when no id provided,
|
159
|
+
# e.g. where(id: nil).
|
160
|
+
def data_for_shift_safe(shift_to:, shift_from:)
|
161
|
+
true if shift_from&.id && shift_to&.id
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
data/lib/shiftable/single.rb
CHANGED
@@ -22,76 +22,57 @@ module Shiftable
|
|
22
22
|
# Inheriting from Module is a powerful pattern. If you like it checkout the debug_logging gem!
|
23
23
|
class Single < Module
|
24
24
|
def initialize(belongs_to:, has_one:, method_prefix: nil, preflight_checks: true, before_shift: nil)
|
25
|
+
# Ruby's Module initializer doesn't take any arguments
|
25
26
|
super()
|
26
|
-
raise ArgumentError, "belongs_to must be a symbol" unless belongs_to.is_a?(Symbol)
|
27
|
-
raise ArgumentError, "has_one must be a symbol" unless has_one.is_a?(Symbol)
|
28
27
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
28
|
+
@signature = ModSignature.new(
|
29
|
+
# For the following, imagine you are a Spaceship Captain, the Spaceship belongs_to you, and it has only one Captain.
|
30
|
+
# But you have to sell it to your nemesis!
|
31
|
+
associations: {
|
32
|
+
# The name of the belongs_to association, defined on the shifting model, e.g. Spaceship
|
33
|
+
# Normally a camel-cased, symbolized, version of the class name.
|
34
|
+
# In the case where Spaceship belongs_to: :captain, this is :captain.
|
35
|
+
belongs_to: belongs_to.to_s.to_sym,
|
36
|
+
# The name of the has_one association, defined on the shift_to/shift_from model, e.g. Captain.
|
37
|
+
# Normally a camel-cased, symbolized, version of the class name.
|
38
|
+
# In the case where Captain has_one: :spaceship, this is :spaceship.
|
39
|
+
has_one: has_one.to_s.to_sym
|
40
|
+
},
|
41
|
+
options: {
|
42
|
+
# Do not move record if a record already exists (we are shifting a "has_one" association, after all)
|
43
|
+
preflight_checks: preflight_checks,
|
44
|
+
method_prefix: method_prefix,
|
45
|
+
# will prevent the save if it returns false
|
46
|
+
# allows for any custom logic to be run, such as setting attributes, prior to the shift (save).
|
47
|
+
before_shift: before_shift
|
48
|
+
},
|
49
|
+
type: :sg
|
50
|
+
)
|
50
51
|
end
|
51
52
|
|
53
|
+
# NOTE: Possible difference in how inheritance works when using extend vs include
|
54
|
+
# with Shiftable::Single.new
|
52
55
|
def extended(base)
|
53
|
-
shift_single_modulizer = ShiftSingleModulizer.to_mod(@
|
54
|
-
@before_shift)
|
56
|
+
shift_single_modulizer = ShiftSingleModulizer.to_mod(@signature.add_base(base))
|
55
57
|
base.singleton_class.send(:prepend, shift_single_modulizer)
|
56
58
|
end
|
57
59
|
|
60
|
+
# NOTE: Possible difference in how inheritance works when using extend vs include
|
61
|
+
# with Shiftable::Single.new
|
58
62
|
def included(base)
|
59
|
-
shift_single_modulizer = ShiftSingleModulizer.to_mod(@
|
60
|
-
@before_shift)
|
63
|
+
shift_single_modulizer = ShiftSingleModulizer.to_mod(@signature.add_base(base))
|
61
64
|
base.send(:prepend, shift_single_modulizer)
|
62
65
|
end
|
63
66
|
|
64
67
|
# Creates anonymous Ruby Modules, containing dynamically built methods
|
65
68
|
module ShiftSingleModulizer
|
66
|
-
def to_mod(
|
69
|
+
def to_mod(signature)
|
67
70
|
Module.new do
|
68
|
-
define_method(:"#{mepr}shift_column") do
|
69
|
-
|
70
|
-
reflection.foreign_key
|
71
|
-
end
|
72
|
-
define_method(:"#{mepr}shift_find") do |id|
|
73
|
-
return nil unless id
|
74
|
-
|
75
|
-
find_by(send("#{mepr}shift_column") => id)
|
71
|
+
define_method(:"#{signature.mepr}shift_column") do
|
72
|
+
signature.shift_column
|
76
73
|
end
|
77
|
-
define_method(:"#{mepr}
|
78
|
-
|
79
|
-
return false if shift_to&.id.nil?
|
80
|
-
|
81
|
-
if preflight_checks
|
82
|
-
already_exists = shift_to.send(has_one)
|
83
|
-
return false if already_exists
|
84
|
-
end
|
85
|
-
|
86
|
-
send("#{mepr}shift_find", shift_from.id)
|
87
|
-
end
|
88
|
-
define_method(:"#{mepr}shift_single") do |shift_to:, shift_from:|
|
89
|
-
shifting = send("#{mepr}shift_safe_find", shift_to: shift_to, shift_from: shift_from)
|
90
|
-
return false unless shifting
|
91
|
-
|
92
|
-
shifting.send("#{send("#{mepr}shift_column")}=", shift_to.id)
|
93
|
-
shifting.save if before_shift.nil? || before_shift.call(shifting: shifting, shift_to: shift_to,
|
94
|
-
shift_from: shift_from)
|
74
|
+
define_method(:"#{signature.mepr}shift_single") do |shift_to:, shift_from:|
|
75
|
+
signature.shift_data!(shift_to: shift_to, shift_from: shift_from)
|
95
76
|
end
|
96
77
|
end
|
97
78
|
end
|
data/lib/shiftable/version.rb
CHANGED
data/lib/shiftable.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "shiftable/version"
|
4
|
-
require_relative "shiftable/
|
4
|
+
require_relative "shiftable/mod_signature"
|
5
5
|
require_relative "shiftable/collection"
|
6
|
+
require_relative "shiftable/single"
|
6
7
|
|
7
8
|
module Shiftable
|
8
9
|
class Error < StandardError; end
|
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.3.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-10-
|
11
|
+
date: 2021-10-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -318,6 +318,7 @@ files:
|
|
318
318
|
- README.md
|
319
319
|
- lib/shiftable.rb
|
320
320
|
- lib/shiftable/collection.rb
|
321
|
+
- lib/shiftable/mod_signature.rb
|
321
322
|
- lib/shiftable/single.rb
|
322
323
|
- lib/shiftable/version.rb
|
323
324
|
homepage: https://railsbling.com/tags/shiftable
|