shiftable 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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