Skip to content

Commit 6fd0f4c

Browse files
author
Carlos Silva
committed
Enum and Enum set fiexes and small additions
1 parent bb6a329 commit 6fd0f4c

File tree

10 files changed

+163
-91
lines changed

10 files changed

+163
-91
lines changed

lib/torque/postgresql/attributes.rb

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,3 @@
44
require_relative 'attributes/enum'
55
require_relative 'attributes/enum_set'
66
require_relative 'attributes/period'
7-
8-
module Torque
9-
module PostgreSQL
10-
module Attributes
11-
extend ActiveSupport::Concern
12-
13-
# Configure enum_save_on_bang behavior
14-
included do
15-
class_attribute :enum_save_on_bang, instance_accessor: true
16-
self.enum_save_on_bang = Torque::PostgreSQL.config.enum.save_on_bang
17-
end
18-
end
19-
20-
ActiveRecord::Base.include Attributes
21-
end
22-
end

lib/torque/postgresql/attributes/builder.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ module Torque
55
module PostgreSQL
66
module Attributes
77
module Builder
8-
def self.include_on(klass, method_name, builder_klass, &block)
8+
def self.include_on(klass, method_name, builder_klass, **extra, &block)
99
klass.define_singleton_method(method_name) do |*args, **options|
1010
return unless connection.table_exists?(table_name)
1111

1212
args.each do |attribute|
1313
begin
1414
# Generate methods on self class
15-
builder = builder_klass.new(self, attribute, options)
15+
builder = builder_klass.new(self, attribute, extra.merge(options))
1616
builder.conflicting?
1717
builder.build
1818

lib/torque/postgresql/attributes/builder/enum.rb

Lines changed: 103 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ module Builder
55
class Enum
66
VALID_TYPES = %i[enum enum_set].freeze
77

8-
attr_accessor :klass, :attribute, :subtype, :options, :values, :enum_module
8+
attr_accessor :klass, :attribute, :subtype, :options, :values,
9+
:klass_module, :instance_module
910

1011
# Start a new builder of methods for enum values on ActiveRecord::Base
1112
def initialize(klass, attribute, options)
@@ -40,28 +41,41 @@ def values_methods
4041

4142
@values_methods = begin
4243
values.map do |val|
43-
val = val.tr('-', '_')
44-
scope = base % val
44+
key = val.downcase.tr('- ', '__')
45+
scope = base % key
4546
ask = scope + '?'
4647
bang = scope + '!'
47-
[val, [scope, ask, bang]]
48+
[key, [scope, ask, bang, val]]
4849
end.to_h
4950
end
5051
end
5152

53+
# Check if it's building the methods for sets
54+
def set_features?
55+
options[:set_features].present?
56+
end
57+
5258
# Check if any of the methods that will be created get in conflict
5359
# with the base class methods
5460
def conflicting?
5561
return if options[:force] == true
5662
attributes = attribute.pluralize
5763

5864
dangerous?(attributes, true)
59-
dangerous?("#{attributes}_options", true)
65+
dangerous?("#{attributes}_keys", true)
6066
dangerous?("#{attributes}_texts", true)
67+
dangerous?("#{attributes}_options", true)
6168
dangerous?("#{attribute}_text")
6269

63-
values_methods.each do |attr, list|
64-
list.map(&method(:dangerous?))
70+
if set_features?
71+
dangerous?("has_#{attributes}", true)
72+
dangerous?("has_any_#{attributes}", true)
73+
end
74+
75+
values_methods.each do |attr, (scope, ask, bang, *)|
76+
dangerous?(scope, true)
77+
dangerous?(bang)
78+
dangerous?(ask)
6579
end
6680
rescue Interrupt => err
6781
raise ArgumentError, <<-MSG.squish
@@ -73,14 +87,16 @@ def conflicting?
7387

7488
# Create all methods needed
7589
def build
76-
@enum_module = Module.new
90+
@klass_module = Module.new
91+
@instance_module = Module.new
7792

7893
plural
7994
stringify
8095
all_values
96+
set_scopes if set_features?
8197

82-
klass.include enum_module
83-
klass.extend enum_module::ClassMethods
98+
klass.extend klass_module
99+
klass.include instance_module
84100
end
85101

86102
private
@@ -96,78 +112,99 @@ def dangerous?(method_name, class_method = false)
96112
raise Interrupt, method_name.to_s
97113
end
98114
end
115+
rescue Interrupt => e
116+
raise e if Torque::PostgreSQL.config.enum.raise_conflicting
117+
type = class_method ? 'class method' : 'instance method'
118+
indicator = class_method ? '.' : '#'
119+
120+
Torque::PostgreSQL.logger.info(<<~MSG.squish)
121+
Creating #{class_method} :#{method_name} for enum.
122+
Overwriting existing method #{klass.name}#{indicator}#{method_name}.
123+
MSG
99124
end
100125

101126
# Create the method that allow access to the list of values
102127
def plural
103-
attr = attribute
104-
enum_klass = subtype.klass
105-
106-
# TODO: Rewrite these as string
107-
enum_module.const_set('ClassMethods', Module.new)
108-
enum_module::ClassMethods.module_eval do
109-
# def self.statuses() statuses end
110-
define_method(attr.pluralize) do
111-
enum_klass.values
112-
end
113-
114-
# def self.statuses_texts() members.map(&:text) end
115-
define_method(attr.pluralize + '_texts') do
116-
enum_klass.members.map do |member|
117-
member.text(attr, self)
118-
end
119-
end
128+
enum_klass = subtype.klass.name
129+
klass_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
130+
def #{attribute.pluralize} # def roles
131+
::#{enum_klass}.values # Enum::Roles.values
132+
end # end
133+
134+
def #{attribute.pluralize}_keys # def roles_keys
135+
::#{enum_klass}.keys # Enum::Roles.keys
136+
end # end
137+
138+
def #{attribute.pluralize}_texts # def roles_texts
139+
::#{enum_klass}.members.map do |member| # Enum::Roles.members do |member|
140+
member.text('#{attribute}', self) # member.text('role', self)
141+
end # end
142+
end # end
143+
144+
def #{attribute.pluralize}_options # def roles_options
145+
#{attribute.pluralize}_texts.zip(::#{enum_klass}.values) # roles_texts.zip(Enum::Roles.values)
146+
end # end
147+
RUBY
148+
end
120149

121-
# def self.statuses_options() statuses_texts.zip(statuses) end
122-
define_method(attr.pluralize + '_options') do
123-
public_send(attr.pluralize + '_texts').zip(enum_klass.values)
124-
end
125-
end
150+
# Create additional methods when the enum is a set, which needs
151+
# better ways to check if values are present or not
152+
def set_scopes
153+
cast_type = subtype.name.chomp('[]')
154+
klass_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
155+
def has_#{attribute.pluralize}(*values) # def has_roles(*values)
156+
attr = arel_attribute('#{attribute}') # attr = arel_attribute('role')
157+
where(attr.contains(::Arel.array(values, cast: '#{cast_type}'))) # where(attr.contains(::Arel.array(values, cast: 'roles')))
158+
end # end
159+
160+
def has_any_#{attribute.pluralize}(*values) # def has_roles(*values)
161+
attr = arel_attribute('#{attribute}') # attr = arel_attribute('role')
162+
where(attr.overlaps(::Arel.array(values, cast: '#{cast_type}'))) # where(attr.overlaps(::Arel.array(values, cast: 'roles')))
163+
end # end
164+
RUBY
126165
end
127166

128167
# Create the method that turn the attribute value into text using
129168
# the model scope
130169
def stringify
131-
attr = attribute
132-
133-
# TODO: Rewrite these as string
134-
enum_module.module_eval do
135-
# def status_text() status.text('status', self) end
136-
define_method("#{attr}_text") { send(attr)&.text(attr, self) }
137-
end
170+
instance_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
171+
def #{attribute}_text # def role_text
172+
#{attribute}.text('#{attribute}', self) # role.text('role', self)
173+
end # end
174+
RUBY
138175
end
139176

140177
# Create all the methods that represent actions related to the
141178
# attribute value
142179
def all_values
143-
attr = attribute
144-
vals = values_methods
145-
146-
enum_klass = subtype.klass
147-
model_klass = klass
148-
149-
# TODO: Rewrite these as string
150-
enum_module.module_eval do
151-
vals.each do |val, list|
152-
# scope :disabled, -> { where(status: 'disabled') }
153-
model_klass.scope list[0], -> do
154-
where(enum_klass.scope(arel_table[attr], val))
155-
end
156-
157-
# def disabled? status.disabled? end
158-
define_method(list[1]) { send(attr).public_send("#{val}?") }
159-
160-
# def disabled!
161-
# changed = send(attr).public_send("#{val}!")
162-
# save! if changed && enum_save_on_bang
163-
# true
164-
define_method(list[2]) do
165-
changed = send(attr).public_send("#{val}!")
166-
return save! if changed && enum_save_on_bang
167-
true
168-
end
169-
end
180+
klass_content = ''
181+
instance_content = ''
182+
enum_klass = subtype.klass.name
183+
184+
values_methods.each do |key, (scope, ask, bang, val)|
185+
klass_content += <<-RUBY
186+
def #{scope} # def admin
187+
attr = arel_attribute('#{attribute}') # attr = arel_attribute('role')
188+
where(::#{enum_klass}.scope(attr, '#{val}')) # where(Enum::Roles.scope(attr, 'admin'))
189+
end # end
190+
RUBY
191+
192+
instance_content += <<-RUBY
193+
def #{ask} # def admin?
194+
#{attribute}.#{key}? # role.admin?
195+
end # end
196+
197+
def #{bang} # admin!
198+
self.#{attribute} = '#{val}' # self.role = 'admin'
199+
return unless #{attribute}_changed? # return unless role_changed?
200+
return save! if Torque::PostgreSQL.config.enum.save_on_bang
201+
true # true
202+
end # end
203+
RUBY
170204
end
205+
206+
klass_module.module_eval(klass_content)
207+
instance_module.module_eval(instance_content)
171208
end
172209
end
173210
end

lib/torque/postgresql/attributes/builder/period.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ def build_method_helper(type, key, args = [])
164164
method_content = define_string_method(method_name, method_content, args)
165165

166166
source_module = send("#{type}_module")
167-
source_module.class_eval(method_content)
167+
source_module.module_eval(method_content)
168168
end
169169

170170
private

lib/torque/postgresql/attributes/enum.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,14 @@ def values
5151
end
5252
end
5353

54+
# List of valus as symbols
55+
def keys
56+
values.map(&:to_sym)
57+
end
58+
5459
# Different from values, it returns the list of items already casted
5560
def members
56-
values.dup.map(&method(:new))
61+
values.map(&method(:new))
5762
end
5863

5964
# Get the list of the values translated by I18n

lib/torque/postgresql/attributes/enum_set.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class << self
1010
include Enumerable
1111

1212
delegate :each, to: :members
13-
delegate :values, :members, :texts, :to_options, :valid?, :size,
13+
delegate :values, :keys, :members, :texts, :to_options, :valid?, :size,
1414
:length, :connection_specification_name, to: :enum_source
1515

1616
# Find or create the class that will handle the value
@@ -28,7 +28,10 @@ def lookup(name, enum_klass)
2828
# Provide a method on the given class to setup which enum sets will be
2929
# manually initialized
3030
def include_on(klass, method_name = nil)
31-
Enum.include_on(klass, method_name || Torque::PostgreSQL.config.enum.set_method)
31+
method_name ||= Torque::PostgreSQL.config.enum.set_method
32+
Builder.include_on(klass, method_name, Builder::Enum, set_features: true) do |builder|
33+
defined_enums[builder.attribute.to_sym] = builder.subtype
34+
end
3235
end
3336

3437
# The original Enum implementation, for individual values
@@ -73,7 +76,7 @@ def power(*values)
7376

7477
# Build an active record scope for a given atribute agains a value
7578
def scope(attribute, value)
76-
attribute.contains(Array.wrap(value))
79+
attribute.contains(::Arel.array(value, cast: enum_source.type_name))
7780
end
7881

7982
private

lib/torque/postgresql/config.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ module PostgreSQL
55
# Stores a version check for compatibility purposes
66
AR521 = (ActiveRecord.gem_version >= Gem::Version.new('5.2.1'))
77

8+
# Use the same logger as the Active Record one
9+
def self.logger
10+
ActiveRecord::Base.logger
11+
end
12+
813
# Allow nested configurations
914
# :TODO: Rely on +inheritable_copy+ to make nested configurations
1015
config.define_singleton_method(:nested) do |name, &block|
@@ -63,6 +68,10 @@ def config.irregular_models=(hash)
6368
# database or not
6469
enum.save_on_bang = true
6570

71+
# Indicates if it should raise errors when a generated method would
72+
# conflict with an existing one
73+
enum.raise_conflicting = false
74+
6675
# Specify the namespace of each enum type of value
6776
enum.namespace = ::Object.const_set('Enum', Module.new)
6877

lib/torque/postgresql/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module Torque
22
module PostgreSQL
3-
VERSION = '1.1.0'
3+
VERSION = '1.1.1'
44
end
55
end

spec/tests/enum_set_spec.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,15 +271,36 @@ def decorate(model, field, options = {})
271271

272272
it 'has all enum set methods' do
273273
expect(subject).to respond_to(:types)
274+
expect(subject).to respond_to(:types_keys)
274275
expect(subject).to respond_to(:types_texts)
275276
expect(subject).to respond_to(:types_options)
277+
278+
expect(subject).to respond_to(:has_types)
279+
expect(subject).to respond_to(:has_any_types)
280+
276281
expect(instance).to respond_to(:types_text)
277282

278283
subject.types.each do |value|
284+
value = value.underscore
279285
expect(subject).to respond_to(value)
280286
expect(instance).to respond_to(value + '?')
281287
expect(instance).to respond_to(value + '!')
282288
end
283289
end
290+
291+
it 'scope the model correctly' do
292+
query = subject.a.to_sql
293+
expect(query).to match(/"courses"."types" @> ARRAY\['A'\]::types\[\]/)
294+
end
295+
296+
it 'has a match all scope' do
297+
query = subject.has_types('B', 'A').to_sql
298+
expect(query).to match(/"courses"."types" @> ARRAY\['B', 'A'\]::types\[\]/)
299+
end
300+
301+
it 'has a match any scope' do
302+
query = subject.has_any_types('B', 'A').to_sql
303+
expect(query).to match(/"courses"."types" && ARRAY\['B', 'A'\]::types\[\]/)
304+
end
284305
end
285306
end

0 commit comments

Comments
 (0)