activerecord-typedstore 0.6.1 → 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -14
- data/activerecord-typedstore.gemspec +5 -5
- data/gemfiles/Gemfile.ar-3.2 +1 -1
- data/gemfiles/Gemfile.ar-4.0 +1 -1
- data/gemfiles/Gemfile.ar-4.1 +1 -1
- data/gemfiles/Gemfile.ar-4.2 +1 -1
- data/gemfiles/Gemfile.ar-edge +3 -2
- data/lib/active_record/typed_store/dsl.rb +18 -16
- data/lib/active_record/typed_store/extension.rb +35 -137
- data/lib/active_record/typed_store/field.rb +76 -0
- data/lib/active_record/typed_store/type.rb +51 -0
- data/lib/active_record/typed_store/typed_hash.rb +13 -18
- data/lib/active_record/typed_store/version.rb +1 -1
- data/spec/active_record/typed_store/typed_hash_spec.rb +6 -1
- data/spec/active_record/typed_store_spec.rb +132 -73
- data/spec/spec_helper.rb +4 -2
- data/spec/support/models.rb +32 -8
- metadata +20 -21
- data/lib/active_record/typed_store/ar_32_fallbacks.rb +0 -95
- data/lib/active_record/typed_store/coder.rb +0 -28
- data/lib/active_record/typed_store/column.rb +0 -86
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 64a197e045ec0a6c19581176c6a62aaf1800ce77
|
4
|
+
data.tar.gz: ffcf6bcc94d684da15ed877cf9bda5244934145d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ac1eca590e2d1e0daea3a72281219ae43d0ad47c1376bd5f155b1e66ef93e8a6cd3f0403d94d7a92de1791846b9b9df19cf1c017138eb37e767d1ad46dda2da5
|
7
|
+
data.tar.gz: 99169aeeafe4c40c39357d16e8cf6dd54de65fa97a9213635a686d5afe0da1848b00735f55fd73c77b4bd8be165c2b949b287d76334a2a82200732800be36f25
|
data/.travis.yml
CHANGED
@@ -1,13 +1,10 @@
|
|
1
1
|
language: ruby
|
2
2
|
|
3
3
|
rvm:
|
4
|
-
- 2.0
|
5
4
|
- 2.1
|
6
|
-
- 2.2
|
5
|
+
- 2.2.3
|
6
|
+
- 2.3.0
|
7
7
|
gemfile:
|
8
|
-
- gemfiles/Gemfile.ar-3.2
|
9
|
-
- gemfiles/Gemfile.ar-4.0
|
10
|
-
- gemfiles/Gemfile.ar-4.1
|
11
8
|
- gemfiles/Gemfile.ar-4.2
|
12
9
|
- gemfiles/Gemfile.ar-edge
|
13
10
|
|
@@ -19,16 +16,8 @@ addons:
|
|
19
16
|
postgresql: 9.3
|
20
17
|
|
21
18
|
sudo: false
|
19
|
+
cache: bundler
|
22
20
|
|
23
21
|
before_script:
|
24
22
|
- mysql -e 'create database typed_store_test;'
|
25
23
|
- psql -c 'create database typed_store_test;' -U postgres
|
26
|
-
|
27
|
-
matrix:
|
28
|
-
allow_failures:
|
29
|
-
- gemfile: gemfiles/Gemfile.ar-edge
|
30
|
-
exclude:
|
31
|
-
- gemfile: gemfiles/Gemfile.ar-edge
|
32
|
-
rvm: 2.1
|
33
|
-
- gemfile: gemfiles/Gemfile.ar-edge
|
34
|
-
rvm: 2.0
|
@@ -18,14 +18,14 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ['lib']
|
20
20
|
|
21
|
-
spec.add_dependency 'activerecord', '>=
|
21
|
+
spec.add_dependency 'activerecord', '>= 4.2', '< 5.1'
|
22
22
|
|
23
23
|
spec.add_development_dependency 'bundler', '~> 1.3'
|
24
24
|
spec.add_development_dependency 'rake', '~> 10'
|
25
25
|
spec.add_development_dependency 'rspec', '~> 3'
|
26
26
|
spec.add_development_dependency 'coveralls', '~> 0'
|
27
|
-
spec.add_development_dependency 'sqlite3', '~>
|
28
|
-
spec.add_development_dependency 'pg', '~> 0'
|
29
|
-
spec.add_development_dependency 'mysql2', '
|
30
|
-
spec.add_development_dependency 'database_cleaner', '~>
|
27
|
+
spec.add_development_dependency 'sqlite3', '~> 1'
|
28
|
+
spec.add_development_dependency 'pg', '~> 0.18'
|
29
|
+
spec.add_development_dependency 'mysql2', '> 0.3'
|
30
|
+
spec.add_development_dependency 'database_cleaner', '~> 1'
|
31
31
|
end
|
data/gemfiles/Gemfile.ar-3.2
CHANGED
data/gemfiles/Gemfile.ar-4.0
CHANGED
data/gemfiles/Gemfile.ar-4.1
CHANGED
data/gemfiles/Gemfile.ar-4.2
CHANGED
data/gemfiles/Gemfile.ar-edge
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
|
-
gem '
|
3
|
+
gem 'activerecord', github: 'rails/rails'
|
4
|
+
gem 'arel', github: 'rails/arel'
|
4
5
|
gem 'bundler', '~> 1.3'
|
5
6
|
gem 'rake'
|
6
7
|
gem 'rspec'
|
7
8
|
gem 'sqlite3'
|
8
9
|
gem 'pg', '~> 0.11'
|
9
|
-
gem 'mysql2'
|
10
|
+
gem 'mysql2', ['>= 0.3.13', '< 0.5']
|
10
11
|
gem 'database_cleaner'
|
11
12
|
gem 'coveralls', require: false
|
@@ -1,29 +1,31 @@
|
|
1
|
-
|
1
|
+
require 'active_record/typed_store/field'
|
2
2
|
|
3
|
+
module ActiveRecord::TypedStore
|
3
4
|
class DSL
|
5
|
+
attr_reader :fields, :coder
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
@accessors = accessors
|
9
|
-
@columns = []
|
7
|
+
def initialize(options)
|
8
|
+
@coder = options.fetch(:coder) { default_coder }
|
9
|
+
@fields = {}
|
10
10
|
yield self
|
11
11
|
end
|
12
12
|
|
13
|
-
def
|
14
|
-
|
13
|
+
def default_coder
|
14
|
+
ActiveRecord::Coders::YAMLColumn.new
|
15
15
|
end
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
@columns << Column.new(name, type, {accessor: @accessors}.merge(options))
|
20
|
-
end
|
17
|
+
def accessors
|
18
|
+
@fields.values.select { |v| v.accessor }.map(&:name)
|
21
19
|
end
|
22
20
|
|
23
|
-
|
24
|
-
@columns << Column.new(name, :decimal, {accessor: @accessors, limit: 20, scale: 6}.merge(options))
|
25
|
-
end
|
21
|
+
delegate :keys, to: :@fields
|
26
22
|
|
23
|
+
NO_DEFAULT_GIVEN = Object.new
|
24
|
+
[:string, :text, :integer, :float, :datetime, :date, :boolean, :decimal, :any].each do |type|
|
25
|
+
define_method(type) do |name, **options|
|
26
|
+
@fields[name] = Field.new(name, type, options)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
alias_method :date_time, :datetime
|
27
30
|
end
|
28
|
-
|
29
31
|
end
|
@@ -1,40 +1,36 @@
|
|
1
|
-
require 'active_record/typed_store/column'
|
2
1
|
require 'active_record/typed_store/dsl'
|
2
|
+
require 'active_record/typed_store/type'
|
3
3
|
require 'active_record/typed_store/typed_hash'
|
4
4
|
|
5
5
|
module ActiveRecord::TypedStore
|
6
|
-
AR_VERSION = Gem::Version.new(ActiveRecord::VERSION::STRING)
|
7
|
-
IS_AR_3_2 = AR_VERSION < Gem::Version.new('4.0')
|
8
|
-
IS_AR_4_1 = AR_VERSION >= Gem::Version.new('4.1') && AR_VERSION < Gem::Version.new('4.2.0')
|
9
|
-
IS_AR_4_2 = AR_VERSION >= Gem::Version.new('4.2.0')
|
10
|
-
|
11
6
|
module Extension
|
12
7
|
extend ActiveSupport::Concern
|
13
8
|
|
14
9
|
included do
|
15
|
-
class_attribute :typed_stores
|
16
|
-
class_attribute :typed_store_attributes, instance_accessor: false
|
10
|
+
class_attribute :typed_stores
|
17
11
|
end
|
18
12
|
|
19
13
|
module ClassMethods
|
14
|
+
def store_accessors
|
15
|
+
return [] unless typed_stores
|
16
|
+
typed_stores.values.map(&:accessors).flatten
|
17
|
+
end
|
20
18
|
|
21
19
|
def typed_store(store_attribute, options={}, &block)
|
22
|
-
dsl = DSL.new(options
|
20
|
+
dsl = DSL.new(options, &block)
|
21
|
+
self.typed_stores = {}
|
22
|
+
self.typed_stores[store_attribute] = dsl
|
23
23
|
|
24
|
-
|
25
|
-
|
24
|
+
typed_klass = TypedHash.create(dsl.fields.values)
|
25
|
+
const_set("#{store_attribute}_hash".camelize, typed_klass)
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
dsl.accessors.each { |c| define_store_attribute_queries(store_attribute, c) }
|
31
|
-
|
32
|
-
dsl
|
27
|
+
attribute(store_attribute, Type.new(typed_klass, dsl.coder))
|
28
|
+
store_accessor(store_attribute, dsl.accessors)
|
33
29
|
end
|
34
30
|
|
35
31
|
def define_attribute_methods
|
36
32
|
super
|
37
|
-
define_typed_store_attribute_methods
|
33
|
+
define_typed_store_attribute_methods if typed_stores
|
38
34
|
end
|
39
35
|
|
40
36
|
def undefine_attribute_methods # :nodoc:
|
@@ -42,28 +38,10 @@ module ActiveRecord::TypedStore
|
|
42
38
|
@typed_store_attribute_methods_generated = false
|
43
39
|
end
|
44
40
|
|
45
|
-
private
|
46
|
-
|
47
|
-
def create_coder(store_attribute, columns)
|
48
|
-
store_class = TypedHash.create(columns)
|
49
|
-
const_set("#{store_attribute}_hash".camelize, store_class)
|
50
|
-
coder_class = Coder.create(store_class)
|
51
|
-
const_set("#{store_attribute}_coder".camelize, coder_class)
|
52
|
-
coder_class
|
53
|
-
end
|
54
|
-
|
55
|
-
def register_typed_store_columns(store_attribute, columns)
|
56
|
-
self.typed_stores ||= {}
|
57
|
-
self.typed_store_attributes ||= {}
|
58
|
-
typed_stores[store_attribute] ||= {}
|
59
|
-
typed_stores[store_attribute].merge!(columns.index_by(&:name))
|
60
|
-
typed_store_attributes.merge!(columns.index_by { |c| c.name.to_s })
|
61
|
-
end
|
62
|
-
|
63
41
|
def define_typed_store_attribute_methods
|
64
42
|
return if @typed_store_attribute_methods_generated
|
65
43
|
store_accessors.each do |attribute|
|
66
|
-
|
44
|
+
define_attribute_method(attribute.to_s)
|
67
45
|
undefine_before_type_cast_method(attribute)
|
68
46
|
end
|
69
47
|
@typed_store_attribute_methods_generated = true
|
@@ -74,120 +52,40 @@ module ActiveRecord::TypedStore
|
|
74
52
|
method = "#{attribute}_before_type_cast"
|
75
53
|
undef_method(method) if method_defined?(method)
|
76
54
|
end
|
77
|
-
|
78
|
-
def store_accessors
|
79
|
-
return [] unless typed_store_attributes
|
80
|
-
typed_store_attributes.values.select(&:accessor?).map(&:name).map(&:to_s)
|
81
|
-
end
|
82
|
-
|
83
|
-
def create_time_zone_conversion_attribute?(name, column)
|
84
|
-
column ||= typed_store_attributes[name]
|
85
|
-
super(name, column)
|
86
|
-
end
|
87
|
-
|
88
|
-
def define_store_attribute_queries(store_attribute, column_name)
|
89
|
-
define_method("#{column_name}?") do
|
90
|
-
query_store_attribute(store_attribute, column_name)
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
end
|
95
|
-
|
96
|
-
protected
|
97
|
-
|
98
|
-
if IS_AR_4_2
|
99
|
-
def attribute_method?(attr_name)
|
100
|
-
super || store_attribute_method?(attr_name)
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
def store_attribute_method?(attr_name)
|
105
|
-
return unless self.class.typed_store_attributes
|
106
|
-
store_attribute = self.class.typed_store_attributes[attr_name]
|
107
|
-
store_attribute && store_attribute.accessor?
|
108
55
|
end
|
109
56
|
|
110
|
-
def
|
111
|
-
|
112
|
-
if column.try(:type) == :datetime && self.class.time_zone_aware_attributes && value.respond_to?(:in_time_zone)
|
113
|
-
value = value.in_time_zone
|
114
|
-
end
|
115
|
-
|
116
|
-
previous_value = read_store_attribute(store_attribute, key)
|
117
|
-
new_value = column ? column.type_cast(value) : value
|
118
|
-
attribute_will_change!(key.to_s) if new_value != previous_value
|
57
|
+
def clear_attribute_change(attr_name)
|
58
|
+
return if self.class.store_accessors.include?(attr_name.to_sym)
|
119
59
|
super
|
120
60
|
end
|
121
61
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
return unless match
|
128
|
-
return if match.target == 'attribute_before_type_cast'.freeze && store_attribute_method?(match.attr_name)
|
129
|
-
match
|
130
|
-
end
|
131
|
-
|
132
|
-
def coder_for(attr_name)
|
133
|
-
column = self.class.columns_hash[attr_name.to_s]
|
134
|
-
return unless column && column.cast_type.is_a?(::ActiveRecord::Type::Serialized)
|
135
|
-
column.cast_type.coder
|
136
|
-
end
|
137
|
-
else
|
138
|
-
def coder_for(attr_name)
|
139
|
-
self.class.serialized_attributes[attr_name]
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def write_attribute(attr_name, value)
|
144
|
-
if coder = coder_for(attr_name)
|
145
|
-
if coder.is_a?(ActiveRecord::TypedStore::Coder)
|
146
|
-
return super(attr_name, coder.as_indifferent_hash(value))
|
147
|
-
end
|
62
|
+
def write_store_attribute(store_attribute, key, value)
|
63
|
+
if typed_stores && typed_stores[store_attribute]
|
64
|
+
prev_value = read_store_attribute(store_attribute, key)
|
65
|
+
new_value = typed_stores[store_attribute].fields[key].cast(value)
|
66
|
+
attribute_will_change!(key.to_s) if new_value != prev_value
|
148
67
|
end
|
149
68
|
|
150
69
|
super
|
151
70
|
end
|
152
71
|
|
153
|
-
def
|
154
|
-
|
155
|
-
|
72
|
+
def query_attribute(attr_name)
|
73
|
+
if self.class.store_accessors.include?(attr_name.to_sym)
|
74
|
+
value = public_send(attr_name)
|
156
75
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
end
|
161
|
-
|
162
|
-
def store_columns(store_attribute)
|
163
|
-
self.class.typed_stores.try(:[], store_attribute)
|
164
|
-
end
|
165
|
-
|
166
|
-
# heavilly inspired from ActiveRecord::Base#query_attribute
|
167
|
-
def query_store_attribute(store_attribute, key)
|
168
|
-
value = read_store_attribute(store_attribute, key)
|
169
|
-
|
170
|
-
case value
|
171
|
-
when true then true
|
172
|
-
when false, nil then false
|
173
|
-
else
|
174
|
-
column = store_column(store_attribute, key)
|
175
|
-
|
176
|
-
if column.number?
|
177
|
-
!value.zero?
|
76
|
+
case value
|
77
|
+
when true then true
|
78
|
+
when false, nil then false
|
178
79
|
else
|
179
|
-
|
80
|
+
if value.respond_to?(:zero?)
|
81
|
+
!value.zero?
|
82
|
+
else
|
83
|
+
!value.blank?
|
84
|
+
end
|
180
85
|
end
|
86
|
+
else
|
87
|
+
super
|
181
88
|
end
|
182
89
|
end
|
183
|
-
|
184
|
-
end
|
185
|
-
|
186
|
-
require 'active_record/typed_store/ar_32_fallbacks' if IS_AR_3_2
|
187
|
-
require 'active_record/typed_store/coder'
|
188
|
-
|
189
|
-
unless IS_AR_3_2
|
190
|
-
ActiveModel::AttributeMethods::ClassMethods.send(:alias_method, :define_virtual_attribute_method, :define_attribute_method)
|
191
90
|
end
|
192
|
-
|
193
91
|
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module ActiveRecord::TypedStore
|
2
|
+
class Field
|
3
|
+
attr_reader :array, :blank, :name, :default, :type, :null, :accessor, :type_sym
|
4
|
+
|
5
|
+
def initialize(name, type, options={})
|
6
|
+
type_options = options.slice(:scale, :limit, :precision)
|
7
|
+
@type = lookup_type(type, type_options)
|
8
|
+
@type_sym = type
|
9
|
+
|
10
|
+
@accessor = options.fetch(:accessor, true)
|
11
|
+
@name = name
|
12
|
+
if options.key?(:default)
|
13
|
+
@default = extract_default(options[:default])
|
14
|
+
end
|
15
|
+
@null = options.fetch(:null, true)
|
16
|
+
@blank = options.fetch(:blank, true)
|
17
|
+
@array = options.fetch(:array, false)
|
18
|
+
end
|
19
|
+
|
20
|
+
def has_default?
|
21
|
+
defined?(@default)
|
22
|
+
end
|
23
|
+
|
24
|
+
def cast(value)
|
25
|
+
casted_value = type_cast(value)
|
26
|
+
if !blank
|
27
|
+
casted_value = default if casted_value.blank?
|
28
|
+
elsif !null
|
29
|
+
casted_value = default if casted_value.nil?
|
30
|
+
end
|
31
|
+
casted_value
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
TYPES = {
|
37
|
+
boolean: ::ActiveRecord::Type::Boolean,
|
38
|
+
integer: ::ActiveRecord::Type::Integer,
|
39
|
+
string: ::ActiveRecord::Type::String,
|
40
|
+
float: ::ActiveRecord::Type::Float,
|
41
|
+
date: ::ActiveRecord::Type::Date,
|
42
|
+
datetime: ::ActiveRecord::Type::DateTime,
|
43
|
+
decimal: ::ActiveRecord::Type::Decimal,
|
44
|
+
any: ::ActiveRecord::Type::Value,
|
45
|
+
}
|
46
|
+
|
47
|
+
def lookup_type(type, options)
|
48
|
+
TYPES.fetch(type).new(**options)
|
49
|
+
end
|
50
|
+
|
51
|
+
def extract_default(value)
|
52
|
+
# 4.2 workaround
|
53
|
+
return value if (type_sym == :string || type_sym == :text) && value.nil?
|
54
|
+
|
55
|
+
type_cast(value)
|
56
|
+
end
|
57
|
+
|
58
|
+
def type_cast(value, arrayize: true)
|
59
|
+
if array && (arrayize || value.is_a?(Array))
|
60
|
+
return [] if arrayize && !value.is_a?(Array)
|
61
|
+
return value.map { |v| type_cast(v, arrayize: false) }
|
62
|
+
end
|
63
|
+
|
64
|
+
# 4.2 workaround
|
65
|
+
if type_sym == :string || type_sym == :text
|
66
|
+
return value.to_s unless value.blank? && (null || array)
|
67
|
+
end
|
68
|
+
|
69
|
+
if type.respond_to?(:cast)
|
70
|
+
type.cast(value)
|
71
|
+
else
|
72
|
+
type.type_cast_from_user(value)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|