enumbler 0.1.0 → 0.2.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: 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
  - - ">="