activerecord-typedstore 0.6.1 → 1.0.0.beta1
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 +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
|