Skip to content

Commit 64f69bf

Browse files
author
Carlos
authored
Merge pull request #27 from crashtech/v0214
Lunching new version
2 parents b4971fd + 3cd27b5 commit 64f69bf

20 files changed

+314
-147
lines changed

gemfiles/Gemfile.rails-5.2

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
source 'https://rubygems.org'
22

3-
gem 'rails', '~> 5.2.1'
3+
gem 'rails', '~> 5.2.2'
44
gem 'pg', '~> 1.1.3'
55
gem "byebug"
66

lib/torque/postgresql/adapter/oid/enum.rb

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ def self.create(row)
1010
new(row['typname'])
1111
end
1212

13+
def self.auto_initialize?
14+
Torque::PostgreSQL.config.enum.initializer
15+
end
16+
1317
def initialize(name)
1418
@name = name
1519
@klass = Attributes::Enum.lookup(name)

lib/torque/postgresql/adapter/oid/interval.rb

+18-7
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,15 @@ def cast(value)
4949
# The value must be Integer when no precision is given
5050
def deserialize(value)
5151
return if value.blank?
52-
parts = ActiveSupport::Duration::ISO8601Parser.new(value).parse!
53-
parts_to_duration(parts)
52+
ActiveSupport::Duration.parse(value)
5453
end
5554

5655
# Uses the ActiveSupport::Duration::ISO8601Serializer
5756
# See ActiveSupport::Duration#iso8601
5857
def serialize(value)
5958
return if value.blank?
6059
value = cast(value) unless value.is_a?(ActiveSupport::Duration)
60+
value = remove_weeks(value) if value.parts.to_h.key?(:weeks)
6161
value.iso8601(precision: @scale)
6262
end
6363

@@ -73,20 +73,31 @@ def assert_valid_value(value)
7373

7474
# Transform a list of parts into a duration object
7575
def parts_to_duration(parts)
76-
parts = parts.to_h.with_indifferent_access.slice(*CAST_PARTS)
76+
parts = parts.to_h.slice(*CAST_PARTS)
7777
return 0.seconds if parts.blank?
7878

7979
seconds = 0
8080
parts = parts.map do |part, num|
8181
num = num.to_i unless num.is_a?(Numeric)
82-
if num > 0
83-
seconds += num.send(part).value
84-
[part.to_sym, num]
85-
end
82+
next if num <= 0
83+
84+
seconds += num.send(part).value
85+
[part.to_sym, num]
8686
end
87+
8788
ActiveSupport::Duration.new(seconds, parts.compact)
8889
end
8990

91+
# As PostgreSQL converts weeks in duration to days, intercept duration
92+
# values with weeks and turn them into days before serializing so it
93+
# won't break because the following issues
94+
# https://github.com/crashtech/torque-postgresql/issues/26
95+
# https://github.com/rails/rails/issues/34655
96+
def remove_weeks(value)
97+
parts = value.parts.dup
98+
parts[:days] += parts.delete(:weeks) * 7
99+
ActiveSupport::Duration.new(value.seconds.to_i, parts)
100+
end
90101
end
91102
end
92103
end

lib/torque/postgresql/attributes.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def method_missing(method_name, *args, &block)
2929
# Use local type map to identify attribute decorator
3030
def define_attribute_method(attribute)
3131
type = attribute_types[attribute]
32-
super unless TypeMap.lookup(type, self, attribute, true)
32+
super unless TypeMap.lookup(type, self, attribute)
3333
end
3434

3535
end

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

+56-47
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@ module Attributes
44
module Builder
55
class Enum
66

7-
attr_accessor :klass, :attribute, :subtype, :initial, :options, :values
7+
attr_accessor :klass, :attribute, :subtype, :options, :values
88

99
# Start a new builder of methods for composite values on
1010
# ActiveRecord::Base
11-
def initialize(klass, attribute, subtype, initial, options)
11+
def initialize(klass, attribute, subtype, options)
1212
@klass = klass
1313
@attribute = attribute.to_s
1414
@subtype = subtype
15-
@initial = initial
1615
@options = options
1716

1817
@values = subtype.klass.values
@@ -53,29 +52,24 @@ def values_methods
5352
# with the base class methods
5453
def conflicting?
5554
return false if options[:force] == true
55+
attributes = attribute.pluralize
5656

57-
dangerous?(attribute.pluralize, true)
58-
dangerous?(attribute + '_text')
57+
dangerous?(attributes, true)
58+
dangerous?("#{attributes}_options", true)
59+
dangerous?("#{attributes}_texts", true)
60+
dangerous?("#{attribute}_text")
5961

6062
values_methods.each do |attr, list|
6163
list.map(&method(:dangerous?))
6264
end
6365

6466
return false
6567
rescue Interrupt => err
66-
if !initial
67-
raise ArgumentError, <<-MSG.strip.gsub(/\n +/, ' ')
68-
#{subtype.class.name} was not able to generate requested
69-
methods because the method #{err} already exists in
70-
#{klass.name}.
71-
MSG
72-
else
73-
warn <<-MSG.strip.gsub(/\n +/, ' ')
74-
#{subtype.class.name} was not able to autoload on
75-
#{klass.name} because the method #{err} already exists.
76-
MSG
77-
return true
78-
end
68+
raise ArgumentError, <<-MSG.strip.gsub(/\n +/, ' ')
69+
#{subtype.class.name} was not able to generate requested
70+
methods because the method #{err} already exists in
71+
#{klass.name}.
72+
MSG
7973
end
8074

8175
# Create all methods needed
@@ -102,45 +96,60 @@ def dangerous?(method_name, class_method = false)
10296

10397
# Create the method that allow access to the list of values
10498
def plural
105-
klass.singleton_class.module_eval <<-STR, __FILE__, __LINE__ + 1
106-
def #{attribute.pluralize} # def statuses
107-
::#{subtype.klass.name}.values # ::Enum::Status.values
108-
end # end
109-
STR
99+
attr = attribute
100+
enum_klass = subtype.klass
101+
klass.singleton_class.module_eval do
102+
# def self.statuses() statuses end
103+
define_method(attr.pluralize) do
104+
enum_klass.values
105+
end
106+
107+
# def self.statuses_texts() members.map(&:text) end
108+
define_method(attr.pluralize + '_texts') do
109+
enum_klass.members.map do |member|
110+
member.text(attr, self)
111+
end
112+
end
113+
114+
# def self.statuses_options() statuses_texts.zip(statuses) end
115+
define_method(attr.pluralize + '_options') do
116+
enum_klass.values
117+
end
118+
end
110119
end
111120

112121
# Create the method that turn the attribute value into text using
113122
# the model scope
114123
def text
115-
klass.module_eval <<-STR, __FILE__, __LINE__ + 1
116-
def #{attribute}_text # def status_text
117-
#{attribute}.text('#{attribute}', self) # status.text('status', self)
118-
end # end
119-
STR
124+
attr = attribute
125+
klass.module_eval do
126+
# def status_text() status.text('status', self) end
127+
define_method("#{attr}_text") { send(attr).text(attr, self) }
128+
end
120129
end
121130

122131
# Create all the methods that represent actions related to the
123132
# attribute value
124133
def all_values
125-
values_methods.each do |val, list|
126-
klass.module_eval <<-STR, __FILE__, __LINE__ + 1
127-
scope :#{list[0]}, -> do # scope :disabled, -> do
128-
where(#{attribute}: '#{val}') # where(status: 'disabled')
129-
end # end
130-
STR
131-
klass.module_eval <<-STR, __FILE__, __LINE__ + 1
132-
def #{list[1]} # def disabled?
133-
#{attribute}.#{val}? # status.disabled?
134-
end # end
135-
136-
def #{list[2]} # def disabled!
137-
if enum_save_on_bang # if enum_save_on_bang
138-
update!(#{attribute}: '#{val}') # update!(status: 'disabled')
139-
else # else
140-
#{attribute}.#{val}! # status.disabled!
141-
end # end
142-
end # end
143-
STR
134+
attr = attribute
135+
vals = values_methods
136+
klass.module_eval do
137+
vals.each do |val, list|
138+
# scope :disabled, -> { where(status: 'disabled') }
139+
scope list[0], -> { where(attr => val) }
140+
141+
# def disabled? status.disabled? end
142+
define_method(list[1]) { send(attr).public_send("#{val}?") }
143+
144+
# def disabled! enum_save_on_bang ? update!(status: 'disabled') : status.disabled! end
145+
define_method(list[2]) do
146+
if enum_save_on_bang
147+
update!(attr => val)
148+
else
149+
send(attr).public_send("#{val}!")
150+
end
151+
end
152+
end
144153
end
145154
end
146155

lib/torque/postgresql/attributes/enum.rb

+24-34
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ def lookup(name)
2222
namespace.const_set(const, Class.new(Enum))
2323
end
2424

25+
# Provide a method on the given class to setup which enums will be
26+
# manually initialized
27+
def include_on(klass)
28+
method_name = Torque::PostgreSQL.config.enum.base_method
29+
klass.singleton_class.class_eval do
30+
define_method(method_name) do |*args, **options|
31+
Torque::PostgreSQL::Attributes::TypeMap.decorate(self, args, **options)
32+
end
33+
end
34+
end
35+
2536
# You can specify the connection name for each enum
2637
def connection_specification_name
2738
return self == Enum ? 'primary' : superclass.connection_specification_name
@@ -47,6 +58,16 @@ def members
4758
values.dup.map(&method(:new))
4859
end
4960

61+
# Get the list of the values translated by I18n
62+
def texts
63+
members.map(&:text)
64+
end
65+
66+
# Get a list of values translated and ready for select
67+
def to_options
68+
texts.zip(values)
69+
end
70+
5071
# Fetch a value from the list
5172
# see https://github.com/rails/rails/blob/v5.0.0/activerecord/lib/active_record/fixtures.rb#L656
5273
# see https://github.com/rails/rails/blob/v5.0.0/activerecord/lib/active_record/validations/uniqueness.rb#L101
@@ -88,21 +109,6 @@ def connection(name)
88109

89110
end
90111

91-
# Extension of the ActiveRecord::Base to initiate the enum features
92-
module Base
93-
94-
method_name = Torque::PostgreSQL.config.enum.base_method
95-
module_eval <<-STR, __FILE__, __LINE__ + 1
96-
def #{method_name}(*args, **options)
97-
args.each do |attribute|
98-
type = attribute_types[attribute.to_s]
99-
TypeMap.lookup(type, self, attribute.to_s, false, options)
100-
end
101-
end
102-
STR
103-
104-
end
105-
106112
# Override string initializer to check for a valid value
107113
def initialize(value)
108114
str_value = value.is_a?(Numeric) ? self.class.values[value.to_i] : value.to_s
@@ -171,7 +177,7 @@ def i18n_keys(attr = nil, model = nil)
171177

172178
if attr && model
173179
values[:attr] = attr
174-
values[:model] = model.class.model_name.i18n_key
180+
values[:model] = model.model_name.i18n_key
175181
list_from = :i18n_scopes
176182
end
177183

@@ -217,32 +223,16 @@ def raise_comparison(other)
217223

218224
end
219225

220-
# Extend ActiveRecord::Base so it can have the initializer
221-
ActiveRecord::Base.extend Enum::Base
222-
223226
# Create the methods related to the attribute to handle the enum type
224-
TypeMap.register_type Adapter::OID::Enum do |subtype, attribute, initial = false, options = nil|
225-
break if initial && !Torque::PostgreSQL.config.enum.initializer
226-
options = {} if options.nil?
227-
227+
TypeMap.register_type Adapter::OID::Enum do |subtype, attribute, options = nil|
228228
# Generate methods on self class
229-
builder = Builder::Enum.new(self, attribute, subtype, initial, options)
229+
builder = Builder::Enum.new(self, attribute, subtype, options || {})
230230
break if builder.conflicting?
231231
builder.build
232232

233233
# Mark the enum as defined
234234
defined_enums[attribute] = subtype.klass
235235
end
236-
237-
# Define a method to find yet to define constants
238-
Torque::PostgreSQL.config.enum.namespace.define_singleton_method(:const_missing) do |name|
239-
Enum.lookup(name)
240-
end
241-
242-
# Define a helper method to get a sample value
243-
Torque::PostgreSQL.config.enum.namespace.define_singleton_method(:sample) do |name|
244-
Enum.lookup(name).sample
245-
end
246236
end
247237
end
248238
end

lib/torque/postgresql/attributes/type_map.rb

+41-3
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,47 @@ def types
1010
@types ||= {}
1111
end
1212

13+
# Store which elements should be initialized
14+
def decorable
15+
@decorable ||= Hash.new{ |h, k| h[k] = [] }
16+
end
17+
18+
# List of options for each individual attribute on each klass
19+
def options
20+
@options ||= Hash.new{ |h, k| h[k] = {} }
21+
end
22+
23+
# Mark the list of attributes on the given class that can be decorated
24+
def decorate(klass, *attributes, **set_options)
25+
attributes.flatten.each do |attribute|
26+
decorable[klass] << attribute.to_s
27+
options[klass][attribute.to_s] = set_options.deep_dup
28+
end
29+
end
30+
31+
# Force the list of attributes on the given class to be decorated by
32+
# this type mapper
33+
def decorate!(klass, *attributes, **options)
34+
decorate(klass, *attributes, **options)
35+
attributes.flatten.map do |attribute|
36+
type = klass.attribute_types[attribute.to_s]
37+
lookup(type, klass, attribute.to_s)
38+
end
39+
end
40+
1341
# Register a type that can be processed by a given block
1442
def register_type(key, &block)
1543
raise_type_defined(key) if present?(key)
1644
types[key] = block
1745
end
1846

1947
# Search for a type match and process it if any
20-
def lookup(key, klass, *args)
21-
return unless present?(key)
22-
klass.instance_exec(key, *args, &types[key.class])
48+
def lookup(key, klass, attribute, *args)
49+
return unless present?(key) && decorable?(key, klass, attribute)
50+
51+
set_options = options[klass][attribute]
52+
args.unshift(set_options) unless set_options.nil?
53+
klass.instance_exec(key, attribute, *args, &types[key.class])
2354
rescue LocalJumpError
2455
# There's a bug or misbehavior that blocks being called through
2556
# instance_exec don't accept neither return nor break
@@ -31,6 +62,13 @@ def present?(key)
3162
types.key?(key.class)
3263
end
3364

65+
# Check whether the given attribute on the given klass is
66+
# decorable by this type mapper
67+
def decorable?(key, klass, attribute)
68+
key.class.auto_initialize? ||
69+
(decorable.key?(klass) && decorable[klass].include?(attribute.to_s))
70+
end
71+
3472
# Message when trying to define multiple types
3573
def raise_type_defined(key)
3674
raise ArgumentError, <<-MSG.strip

0 commit comments

Comments
 (0)