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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 769e7ab8e27a7ea4a5ea0e9ea2b7efeda58865f11a15f6504e0e00f6db8f9ce9
4
- data.tar.gz: 3caa7d81b477be16f016358e6412a300782688632ea640685bdda70e63df6231
3
+ metadata.gz: 15d27c7c2706faf797241101fa5229f4bf707b06f4d1b7c839cc66fd7ab2ecf8
4
+ data.tar.gz: 0fa0fbe15096d109c2d63568101ad0589b4c684775b69b95e831289b8bfdac3d
5
5
  SHA512:
6
- metadata.gz: 3055e1d7bb53da73830429a6d942c992aea32991412b3c97a281f5559a151a6ed9cf8f11cad868c83a7c329886f97c75db3cd69d4eb7bef5c8cd7114a9b3e894
7
- data.tar.gz: e060753ad1c7190ca7047fa12040f6799f43d425762414e0201d424f63291f425c7354b7377fb3cc6fbee662ebc328b54fc63debbcc084fba08b2fcadfbac95c
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
@@ -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
- def initialize(belongs_to:, has_many:, method_prefix: nil, preflight_checks: true, before_shift: nil)
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
- @method_prefix = method_prefix
37
-
38
- # will prevent the save if it returns false
39
- # allows for any custom logic to be run, such as setting shift_from attributes, prior to the shift is saved.
40
- @before_shift = before_shift
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(@belongs_to, @has_many, @method_prefix,
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(@belongs_to, @has_many, @method_prefix,
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(belongs_to, has_many, mepr, before_shift)
63
+ def to_mod(signature)
58
64
  Module.new do
59
- define_method(:"#{mepr}shift_cx_column") do
60
- reflection = reflect_on_association(belongs_to).klass.reflect_on_association(has_many)
61
- reflection.foreign_key
65
+ define_method(:"#{signature.mepr}shift_cx_column") do
66
+ signature.shift_column
62
67
  end
63
- define_method(:"#{mepr}shift_cx_relation") do |id|
64
- return nil unless id
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
@@ -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
- # 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
- #
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
36
-
37
- # The name of the has_one association, defined on the shift_to/shift_from model, e.g. Captain.
38
- # Normally a camel-cased, symbolized, version of the class name.
39
- # In the case where Captain has_one: :spaceship, this is :spaceship.
40
- @has_one = has_one
41
-
42
- @method_prefix = method_prefix
43
-
44
- # Do not move record if a record already exists (we are shifting a "has_one" association, after all)
45
- @preflight_checks = preflight_checks
46
-
47
- # will prevent the save if it returns false
48
- # allows for any custom logic to be run, such as setting shift_from attributes, prior to the shift is saved.
49
- @before_shift = before_shift
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(@belongs_to, @has_one, @method_prefix, @preflight_checks,
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(@belongs_to, @has_one, @method_prefix, @preflight_checks,
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(belongs_to, has_one, mepr, preflight_checks, before_shift)
69
+ def to_mod(signature)
67
70
  Module.new do
68
- define_method(:"#{mepr}shift_column") do
69
- reflection = reflect_on_association(belongs_to).klass.reflect_on_association(has_one)
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}shift_safe_find") do |shift_to:, shift_from:|
78
- return false if shift_from&.id.nil?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shiftable
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
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/single"
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.2.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-24 00:00:00.000000000 Z
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