enumbler 0.1.0 → 0.2.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: ac96c64c119c34aa4efbc77eaa1caa227d213e1efd631fac9089dbcb70f3f04b
4
- data.tar.gz: 5e2d1125473c01f1e05728cf0f26f7dea219a7755da4db844535164156a6b104
3
+ metadata.gz: 7cd7368add18afee559a79a18b15a28f3aa19514ce3037f88dc1425781a9dd85
4
+ data.tar.gz: d76ebcf6de0265bdd3cee78e745cafb9e81fe6edbdf6ff91ca93cf968d3243e4
5
5
  SHA512:
6
- metadata.gz: 40297daeee09865736136e47095f68e7540e91bed081da0613e2e33558c127c7fe4521c8233e6d1d426cc7c37bb3e147d1216ed9f0684a039a89ab59199c3256
7
- data.tar.gz: cff2b4a1a1d94cd67dba49f141a0ed3b8820ffed03de69edebee6a59a47a0a84b4e3a581d418bf174ebb523353e9ffc03e27de3ebaa61e408c10d1ed9a17c580
6
+ metadata.gz: f66a6d30caeced56ca38e50d70c4d892ae96c91a11208693c6a23e6561f02ffdce820f6dfb85deabb8a96bed52927e88d4321c9b0891041f4085495a7c1293a0
7
+ data.tar.gz: 38d99ad670bbf880cc7f3ef5bf5775f8aa3eec1b8511518f61fbfeaafa5ede38be3d7db49a1c67cd99386b199be132e205764b30a095eccf841e6fe44b481f63
@@ -13,7 +13,7 @@ jobs:
13
13
 
14
14
  strategy:
15
15
  matrix:
16
- ruby_version: [2.5, 2.6]
16
+ ruby_version: [2.5, 2.6, 2.7]
17
17
 
18
18
  steps:
19
19
  - uses: actions/checkout@v2
@@ -37,6 +37,8 @@ jobs:
37
37
  bundle exec rspec
38
38
 
39
39
  build:
40
+ needs: rspec
41
+
40
42
  name: Publish Gem
41
43
  runs-on: ubuntu-latest
42
44
 
@@ -46,7 +48,7 @@ jobs:
46
48
  - name: Set up Ruby
47
49
  uses: actions/setup-ruby@v1
48
50
  with:
49
- version: 2.7.x
51
+ ruby-version: 2.7.x
50
52
 
51
53
  - name: Publish to RubyGems
52
54
  run: |
data/.rubocop.yml CHANGED
@@ -21,6 +21,9 @@ Lint/RaiseException:
21
21
  Lint/StructNewOverride:
22
22
  Enabled: true
23
23
 
24
+ Style/Documentation:
25
+ Enabled: false
26
+
24
27
  Style/HashEachMethods:
25
28
  Enabled: true
26
29
 
@@ -38,20 +41,5 @@ Style/TrailingCommaInArrayLiteral:
38
41
  Enabled: true
39
42
  EnforcedStyleForMultiline: consistent_comma
40
43
 
41
- Metrics/BlockLength:
44
+ Metrics:
42
45
  Enabled: false
43
-
44
- Metrics/AbcSize:
45
- Enabled: false
46
-
47
- Metrics/CyclomaticComplexity:
48
- Enabled: false
49
-
50
- Metrics/MethodLength:
51
- Max: 30
52
-
53
- Metrics/ModuleLength:
54
- Max: 300
55
-
56
- Metrics/ClassLength:
57
- Max: 300
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- enumbler (0.1.0)
4
+ enumbler (0.2.0)
5
5
  activerecord (>= 6.0)
6
6
  activesupport (>= 6.0)
7
7
 
data/README.md CHANGED
@@ -1,6 +1,56 @@
1
1
  # Enumbler
2
2
 
3
- `Enums` are terrific, but they lack integrity. Enumbler!
3
+ `Enums` are terrific, but they lack integrity. Enumbler! The _enum enabler_! The goal is to allow one to maintain a true foreign_key database driven relationship that also behaves a little bit like an `enum`. Best of both worlds? We hope so.
4
+
5
+
6
+ ## Example
7
+
8
+ Suppose you have a `House` and you want to add some `colors` to the house. You are tempted to use an `enum` but the `Enumbler` is calling!
9
+
10
+ ```ruby
11
+ ActiveRecord::Schema.define do
12
+ create_table :colors|t|
13
+ t.string :label, null: false
14
+ end
15
+
16
+ create_table :houses|t|
17
+ t.references :color, foreign_key: true, null: false
18
+ end
19
+ end
20
+
21
+ class ApplicationRecord < ActiveRecord::Base
22
+ include Enumbler
23
+ self.abstract_class = true
24
+ end
25
+
26
+ # Our Color has been Enumbled with some basic colors.
27
+ class Color < ApplicationRecord
28
+ include Enumbler::Enabler
29
+
30
+ enumble :black, 1
31
+ enumble :white, 2
32
+ enumble :dark_brown, 3
33
+ enumble :infinity, 4, label: 'Infinity - and beyond!'
34
+ end
35
+
36
+ # Our House class, it has a color of course!
37
+ class House < ApplicationRecord
38
+ enumbled_to :color
39
+ end
40
+
41
+ # This gives you some power:
42
+ Color::BLACK # => 1
43
+ Color.black # => equivalent to Color.find(1)
44
+ Color.black.black? # => true
45
+ Color.black.is_black # => true
46
+ Color.white.not_black? # => true
47
+
48
+ house = House.create!(color: Color.black)
49
+ house.black?
50
+ house.not_black?
51
+
52
+ House.color(:black) # => [house]
53
+ ```
4
54
 
5
55
  ## Installation
6
56
 
data/enumbler.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.description = 'A more complete description is forthcoming.'
13
13
  spec.homepage = 'https://github.com/linguabee/enumbler'
14
14
  spec.license = 'MIT'
15
- spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
16
16
 
17
17
  spec.metadata['allowed_push_host'] = 'https://rubygems.org'
18
18
 
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enumbler
4
+ module Enabler
5
+ extend ActiveSupport::Concern
6
+
7
+ # The Enumble definition that this record defined.
8
+ # @return [Enumbler::Enumble]
9
+ def enumble
10
+ @enumble = self.class.enumbles.find { |enumble| enumble.id == id }
11
+
12
+ raise Error, 'An enumble is not defined for this record!' if @enumble.nil?
13
+
14
+ @enumble
15
+ end
16
+
17
+ # These ClassMethods can be included in any model that you wish to
18
+ # _Enumble_!
19
+ #
20
+ # class Color < ApplicationRecord
21
+ # include Enumbler::Enabler
22
+ #
23
+ # enumble :black, 1
24
+ # enumble :white, 2
25
+ # end
26
+ #
27
+ module ClassMethods
28
+ attr_reader :enumbles
29
+
30
+ # Defines an Enumble for this model. An enum with integrity.
31
+ #
32
+ # # in your migration
33
+ # create_table :colors, force: true do |t|
34
+ # t.string :label, null: false
35
+ # end
36
+ #
37
+ # class Color < ApplicationRecord
38
+ # include Enumbler::Enabler
39
+ #
40
+ # enumble :black, 1
41
+ # enumble :white, 2
42
+ # enumble :dark_brown, 3, # label: 'dark-brown'
43
+ # enumble :black_hole, 3, label: 'Oh my! It is a black hole!'
44
+ # end
45
+ #
46
+ # # Dynamically adds the following methods:
47
+ # Color::BLACK #=> 1
48
+ # Color.black #=> Color.find(1)
49
+ # color.black? #=> true || false
50
+ # color.is_black #=> true || false
51
+ #
52
+ # @param enum [Symbol] the enum representation
53
+ # @param id [Integer] the primary key value
54
+ # @param label [String] optional: label for humans
55
+ # @param **options [Hash] optional: additional attributes and values that
56
+ # will be saved to the database for this enumble record
57
+ def enumble(enum, id, label: nil, **options)
58
+ @enumbles ||= []
59
+ @enumbled_model = self
60
+
61
+ enumble = Enumble.new(enum, id, label: label, **options)
62
+
63
+ if @enumbles.include?(enumble)
64
+ raise Error, "You cannot add the same Enumble twice! Attempted to add: #{enum}, #{id}."
65
+ end
66
+
67
+ define_dynamic_methods_and_constants_for_enumbled_model(enum, id)
68
+
69
+ @enumbles << enumble
70
+ end
71
+
72
+ # Return the record id(s) based on different argument types. Can accept an
73
+ # Integer, a Symbol, or an instance of Enumbled model. This lookup is a
74
+ # databse-free lookup.
75
+ #
76
+ # Color.ids_from_enumablable(1, 2) # => [1, 2]
77
+ # Color.ids_from_enumablable(:black, :white) # => [1, 2]
78
+ # Color.ids_from_enumablable(Color.black, Color.white) # => [1, 2]
79
+ #
80
+ # @raise [Error] when there is no enumble to be found
81
+ # @param *args [Integer, Symbol, Class]
82
+ # @return [Array<Integer>]
83
+ def ids_from_enumablable(*args)
84
+ args.flatten.compact.uniq.map do |arg|
85
+ err = "Unable to find a #{@enumbled_model}#enumble with #{arg}"
86
+
87
+ begin
88
+ arg = Integer(arg) # raises Type error if not a real integer
89
+ enumble = @enumbled_model.enumbles.find { |e| e.id == arg }
90
+ rescue TypeError
91
+ enumble = if arg.is_a?(Symbol)
92
+ @enumbled_model.enumbles.find { |e| e.enum == arg }
93
+ elsif arg.instance_of?(@enumbled_model)
94
+ arg.enumble
95
+ end
96
+ end
97
+
98
+ enumble&.id || raise(Error, err)
99
+ rescue Error
100
+ raise Error, err
101
+ end
102
+ end
103
+
104
+ # Seeds the database with the Enumble data.
105
+ # @param delete_missing_records [Boolean] remove any records that are no
106
+ # longer defined (default: false)
107
+ def seed_the_enumbler(delete_missing_records: false)
108
+ max_database_id = all.order('id desc').take&.id || 0
109
+ max_enumble_id = enumbles.map(&:id).max
110
+
111
+ max_id = max_enumble_id > max_database_id ? max_enumble_id : max_database_id
112
+
113
+ discarded_ids = []
114
+
115
+ (1..max_id).each do |id|
116
+ enumble = @enumbles.find { |e| e.id == id }
117
+
118
+ if enumble.nil?
119
+ discarded_ids << id
120
+ next
121
+ end
122
+
123
+ record = find_or_initialize_by(id: id)
124
+ record.attributes = enumble.attributes
125
+ record.save!
126
+ end
127
+
128
+ where(id: discarded_ids).delete_all if delete_missing_records
129
+ end
130
+
131
+ # Seeds the database with the Enumble data, removing any records that are no
132
+ # longer defined.
133
+ def seed_the_enumbler!
134
+ seed_the_enumbler(delete_missing_records: true)
135
+ end
136
+
137
+ private
138
+
139
+ def define_dynamic_methods_and_constants_for_enumbled_model(enum, id)
140
+ method_name = "#{enum}?"
141
+ not_method_name = "not_#{enum}?"
142
+ alias_method_name = "is_#{enum}"
143
+
144
+ const_set(enum.to_s.upcase, id)
145
+ define_method(method_name) { self.id == id }
146
+ define_method(not_method_name) { self.id != id }
147
+ alias_method alias_method_name, method_name
148
+ define_singleton_method(enum) { find(id) }
149
+ end
150
+ end
151
+ end
152
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Enumbler
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/enumbler.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'enumbler/enumble'
4
+ require 'enumbler/enabler'
4
5
  require 'enumbler/version'
5
6
 
6
7
  require 'active_support/concern'
@@ -10,65 +11,23 @@ require 'active_support/inflector'
10
11
  module Enumbler
11
12
  extend ActiveSupport::Concern
12
13
 
14
+ # An error raised by the Enumbler.
13
15
  class Error < StandardError; end
14
16
 
15
- # The Enumble definition that this record defined.
16
- # @return [Enumbler::Enumble]
17
- def enumble
18
- @enumble = self.class.enumbles.find { |enumble| enumble.id == id }
19
-
20
- raise Error, 'An enumble is not defined for this record!' if @enumble.nil?
21
-
22
- @enumble
23
- end
24
-
25
- # These are the ClassMethods that are added to an ApplicationRecord model when
26
- # `include Enumbler` is added to the class.
17
+ # Include these ClassMethods in your base ApplicationRecord model to bestow
18
+ # any of your models with the ability to be connected to an Enumbled relation
19
+ # in the same way you would use `belongs_to` now you use `enumbled_to`.
20
+ #
21
+ # class ApplicationRecord > ActiveRecord::Base
22
+ # include Enumbler
23
+ # self.abstract_class = true
24
+ # end
25
+ #
26
+ # class House < ApplicationRecord
27
+ # enumbled_to :color
28
+ # end
29
+ #
27
30
  module ClassMethods
28
- attr_reader :enumbles
29
-
30
- # Defines an Enumble for this model. An enum with integrity.
31
- #
32
- # # in your migration
33
- # create_table :colors, force: true do |t|
34
- # t.string :label, null: false
35
- # end
36
- #
37
- # class Color < ApplicationRecord
38
- # include Enumbler
39
- #
40
- # enumble :black, 1
41
- # enumble :white, 2
42
- # enumble :dark_brown, 3, # label: 'dark-brown'
43
- # enumble :black_hole, 3, label: 'Oh my! It is a black hole!'
44
- # end
45
- #
46
- # # Dynamically adds the following methods:
47
- # Color::BLACK #=> 1
48
- # Color.black #=> MyRecord.find(1)
49
- # color.black? #=> true || false
50
- # color.is_black #=> true || false
51
- #
52
- # @param enum [Symbol] the enum representation
53
- # @param id [Integer] the primary key value
54
- # @param label [String] optional: label for humans
55
- # @param **options [Hash] optional: additional attributes and values that
56
- # will be saved to the database for this enumble record
57
- def enumble(enum, id, label: nil, **options)
58
- @enumbles ||= []
59
- @enumbled_model = self
60
-
61
- enumble = Enumble.new(enum, id, label: label, **options)
62
-
63
- if @enumbles.include?(enumble)
64
- raise Error, "You cannot add the same Enumble twice! Attempted to add: #{enum}, #{id}."
65
- end
66
-
67
- define_dynamic_methods_and_constants_for_enumbled_model(enum, id)
68
-
69
- @enumbles << enumble
70
- end
71
-
72
31
  # Defines the relationship between a model and the Enumbled class. Use this
73
32
  # in lieu of `belongs_to` to establish that relationship. It requires a
74
33
  # model that has defined one or more `Enumbles`.
@@ -78,117 +37,59 @@ module Enumbler
78
37
  # t.references :color, foreign_key: true, null: false
79
38
  # end
80
39
  #
81
- # class House < ApplicationRecord
40
+ # class ApplicationRecord > ActiveRecord::Base
82
41
  # include Enumbler
42
+ # self.abstract_class = true
43
+ # end
44
+ #
45
+ # class House < ApplicationRecord
83
46
  # enumbled_to :color
84
47
  # end
85
48
  #
86
49
  # @param name [Symbol] symbol representation of the class this belongs_to
87
- # @param *args [Array] additional arguments passed to `belongs_to`
88
- def enumbled_to(name, scope = nil, prefix: nil, **options)
50
+ # @param prefix [Boolean] default: false; prefix the instance method
51
+ # attributes with the Enumble name, ex: `House.color_black?` instead of
52
+ # `House.black?`
53
+ # @param scope_prefix [string] optional, prefix the class scopes, for
54
+ # example: `where_by` will make it `House.where_by_color(:black)`
55
+ # @param **options [Hash] additional options passed to `belongs_to`
56
+ def enumbled_to(name, scope = nil, prefix: false, scope_prefix: nil, **options)
89
57
  class_name = name.to_s.classify
90
- @enumbled_model = class_name.constantize
58
+ enumbled_model = class_name.constantize
91
59
 
92
- unless @enumbled_model.respond_to?(:enumbles)
93
- raise Error, "The class #{class_name} does not have any enumbles defined."\
60
+ unless enumbled_model.respond_to?(:enumbles)
61
+ raise Error, "The model #{class_name} does not have any enumbles defined."\
94
62
  " You can add them via `#{class_name}.enumble :blue, 1`."
95
63
  end
96
64
 
97
65
  belongs_to(name, scope, **options)
98
66
 
99
- define_dynamic_methods_for_enumbled_to_models(prefix: prefix)
67
+ define_dynamic_methods_for_enumbled_to_models(enumbled_model, prefix: prefix, scope_prefix: scope_prefix)
100
68
  rescue NameError
101
- raise Error, "The class #{class_name} cannot be found. Uninitialized constant."
102
- end
103
-
104
- # Return the record id(s) based on different argument types. Can accept an
105
- # Integer, a Symbol, or an instance of Enumbled model. This lookup is a
106
- # databse-free lookup.
107
- #
108
- # Color.ids_from_enumablable(1, 2) # => [1, 2]
109
- # Color.ids_from_enumablable(:black, :white) # => [1, 2]
110
- # Color.ids_from_enumablable(Color.black, Color.white) # => [1, 2]
111
- #
112
- # @raise [Error] when there is no enumble to be found
113
- # @param *args [Integer, Symbol, Class]
114
- # @return [Array<Integer>]
115
- def ids_from_enumablable(*args)
116
- args.flatten.compact.uniq.map do |arg|
117
- err = "Unable to find a #{@enumbled_model}#enumble with #{arg}"
118
-
119
- begin
120
- arg = Integer(arg) # raises Type error if not a real integer
121
- enumble = @enumbled_model.enumbles.find { |e| e.id == arg }
122
- rescue TypeError
123
- enumble = if arg.is_a?(Symbol)
124
- @enumbled_model.enumbles.find { |e| e.enum == arg }
125
- elsif arg.instance_of?(@enumbled_model)
126
- arg.enumble
127
- end
128
- end
129
-
130
- enumble&.id || raise(Error, err)
131
- rescue Error
132
- raise Error, err
133
- end
134
- end
135
-
136
- # Seeds the database with the Enumble data.
137
- # @param delete_missing_records [Boolean] remove any records that are no
138
- # longer defined (default: false)
139
- def seed_the_enumbler(delete_missing_records: false)
140
- max_database_id = all.order('id desc').take&.id || 0
141
- max_enumble_id = enumbles.map(&:id).max
142
-
143
- max_id = max_enumble_id > max_database_id ? max_enumble_id : max_database_id
144
-
145
- discarded_ids = []
146
-
147
- (1..max_id).each do |id|
148
- enumble = @enumbles.find { |e| e.id == id }
149
-
150
- if enumble.nil?
151
- discarded_ids << id
152
- next
153
- end
154
-
155
- record = find_or_initialize_by(id: id)
156
- record.attributes = enumble.attributes
157
- record.save!
158
- end
159
-
160
- where(id: discarded_ids).delete_all if delete_missing_records
161
- end
162
-
163
- # Seeds the database with the Enumble data, removing any records that are no
164
- # longer defined.
165
- def seed_the_enumbler!
166
- seed_the_enumbler(delete_missing_records: true)
69
+ raise Error, "The model #{class_name} cannot be found. Uninitialized constant."
167
70
  end
168
71
 
169
72
  private
170
73
 
171
- def define_dynamic_methods_and_constants_for_enumbled_model(enum, id)
172
- method_name = "#{enum}?"
173
- alias_method_name = "is_#{enum}"
174
-
175
- const_set(enum.to_s.upcase, id)
176
- define_method(method_name) { self.id == id }
177
- alias_method alias_method_name, method_name
178
- define_singleton_method(enum) { find(id) }
179
- end
180
-
181
- def define_dynamic_methods_for_enumbled_to_models(prefix: nil)
182
- model_name = @enumbled_model.to_s.underscore
183
-
184
- method = if prefix.blank?
185
- model_name
186
- else
187
- "#{prefix}_#{model_name}"
188
- end
74
+ # Define the dynamic methods for this relationship.
75
+ #
76
+ # @todo - we should check for naming conflicts!
77
+ # dangerous_attribute_method?(method_name)
78
+ # method_defined_within?(method_name, self, Module)
79
+ def define_dynamic_methods_for_enumbled_to_models(enumbled_model, prefix: false, scope_prefix: nil)
80
+ model_name = enumbled_model.to_s.underscore
81
+ column_name = "#{model_name}_id"
82
+
83
+ cmethod = scope_prefix.blank? ? model_name : "#{scope_prefix}_#{model_name}"
84
+ define_singleton_method(cmethod) do |*args|
85
+ where(column_name => enumbled_model.ids_from_enumablable(args))
86
+ end
189
87
 
190
- define_singleton_method(method) do |*args|
191
- where("#{model_name}_id": ids_from_enumablable(args))
88
+ enumbled_model.enumbles.each do |enumble|
89
+ method_name = prefix ? "#{model_name}_#{enumble.enum}?" : "#{enumble.enum}?"
90
+ not_method_name = prefix ? "#{model_name}_not_#{enumble.enum}?" : "not_#{enumble.enum}?"
91
+ define_method(method_name) { self[column_name] == enumble.id }
92
+ define_method(not_method_name) { self[column_name] != enumble.id }
192
93
  end
193
94
  end
194
95
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: enumbler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damon Timm
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-04-10 00:00:00.000000000 Z
11
+ date: 2020-04-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -144,6 +144,7 @@ files:
144
144
  - bin/setup
145
145
  - enumbler.gemspec
146
146
  - lib/enumbler.rb
147
+ - lib/enumbler/enabler.rb
147
148
  - lib/enumbler/enumble.rb
148
149
  - lib/enumbler/version.rb
149
150
  homepage: https://github.com/linguabee/enumbler
@@ -162,7 +163,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
162
163
  requirements:
163
164
  - - ">="
164
165
  - !ruby/object:Gem::Version
165
- version: 2.3.0
166
+ version: 2.5.0
166
167
  required_rubygems_version: !ruby/object:Gem::Requirement
167
168
  requirements:
168
169
  - - ">="