activerecord-typedstore 1.1.3 → 1.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +22 -11
- data/README.md +5 -19
- data/activerecord-typedstore.gemspec +3 -3
- data/gemfiles/Gemfile.ar-5.0 +3 -8
- data/gemfiles/Gemfile.ar-5.2 +3 -9
- data/gemfiles/Gemfile.ar-6.0 +5 -0
- data/gemfiles/Gemfile.ar-master +6 -0
- data/lib/active_record/typed_store.rb +3 -1
- data/lib/active_record/typed_store/behavior.rb +75 -0
- data/lib/active_record/typed_store/dsl.rb +3 -1
- data/lib/active_record/typed_store/extension.rb +27 -99
- data/lib/active_record/typed_store/field.rb +3 -0
- data/lib/active_record/typed_store/identity_coder.rb +15 -0
- data/lib/active_record/typed_store/type.rb +2 -0
- data/lib/active_record/typed_store/typed_hash.rb +5 -0
- data/lib/active_record/typed_store/version.rb +3 -1
- data/lib/activerecord-typedstore.rb +2 -0
- data/spec/active_record/typed_store/typed_hash_spec.rb +20 -0
- data/spec/active_record/typed_store_spec.rb +57 -79
- data/spec/support/models.rb +10 -47
- metadata +16 -21
- data/gemfiles/Gemfile.ar-4.2 +0 -11
- data/gemfiles/Gemfile.ar-5.1 +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 922ee9aa28151d4d9682563fb960a01e3f0b483d1c9d1e9168c095391b87344e
|
4
|
+
data.tar.gz: 1116257100c7775ec75c3742666c9ab35454ceca71bef07781fe8581edaeeeec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c01c5370bc89c17b38a7f9a928e96064cececa214eb9e43a90a10b0ee8e990d18fbea8d18f1c45e1b1f3a9446d17174e79fe5483b1ae5a89ac5f5e994e618a8
|
7
|
+
data.tar.gz: 99fc46ff54dd8ba238ef3bda41d2861d200fb8f11f41a5b07d0c5846d747435c3424a5f550c168c6f73a36c78638f6affbf95636f4a75c56af40ad8d42685268
|
data/.travis.yml
CHANGED
@@ -1,22 +1,33 @@
|
|
1
|
+
sudo: false
|
1
2
|
language: ruby
|
3
|
+
cache: bundler
|
4
|
+
|
5
|
+
before_install:
|
6
|
+
- gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
|
7
|
+
- gem install bundler -v '< 2'
|
2
8
|
|
3
9
|
rvm:
|
4
|
-
- 2.
|
5
|
-
- 2.
|
10
|
+
- "2.5"
|
11
|
+
- "2.6"
|
6
12
|
gemfile:
|
7
|
-
- gemfiles/Gemfile.ar-
|
8
|
-
- gemfiles/Gemfile.ar-
|
9
|
-
- gemfiles/Gemfile.ar-
|
13
|
+
- gemfiles/Gemfile.ar-5.2
|
14
|
+
- gemfiles/Gemfile.ar-6.0
|
15
|
+
- gemfiles/Gemfile.ar-master
|
10
16
|
|
11
17
|
env:
|
12
|
-
- TIMEZONE_AWARE=1 POSTGRES=1 MYSQL=1
|
13
|
-
- TIMEZONE_AWARE=0 POSTGRES=1 MYSQL=1
|
18
|
+
- TIMEZONE_AWARE=1 POSTGRES=1 MYSQL=1 POSTGRES_JSON=1
|
19
|
+
- TIMEZONE_AWARE=0 POSTGRES=1 MYSQL=1 POSTGRES_JSON=1
|
14
20
|
|
15
|
-
|
16
|
-
|
21
|
+
matrix:
|
22
|
+
exclude:
|
23
|
+
- rvm: "2.6"
|
24
|
+
gemfile: 'gemfiles/Gemfile.6-0'
|
25
|
+
- rvm: "2.6"
|
26
|
+
gemfile: 'gemfiles/Gemfile.ar-master'
|
17
27
|
|
18
|
-
|
19
|
-
|
28
|
+
services:
|
29
|
+
- mysql
|
30
|
+
- postgresql
|
20
31
|
|
21
32
|
before_script:
|
22
33
|
- mysql -e 'create database typed_store_test;'
|
data/README.md
CHANGED
@@ -94,10 +94,10 @@ end
|
|
94
94
|
|
95
95
|
```
|
96
96
|
|
97
|
-
Type casting rules and attribute behavior are exactly the same as
|
98
|
-
Actually the only difference is that you
|
97
|
+
Type casting rules and attribute behavior are exactly the same as for real database columns.
|
98
|
+
Actually the only difference is that you won't be able to query on these attributes (unless you use JSON or Postgres HStore types) and that you don't need to do a migration to add / remove an attribute.
|
99
99
|
|
100
|
-
If not, please fill an issue.
|
100
|
+
If not, then please fill in an issue.
|
101
101
|
|
102
102
|
## Serialization methods
|
103
103
|
|
@@ -123,21 +123,7 @@ typed_store :settings, coder: Base64MarshalCoder do |s|
|
|
123
123
|
end
|
124
124
|
```
|
125
125
|
|
126
|
-
If you want to use
|
127
|
-
```ruby
|
128
|
-
module DumbCoder
|
129
|
-
extend self
|
130
|
-
|
131
|
-
def load(data)
|
132
|
-
data || {}
|
133
|
-
end
|
134
|
-
|
135
|
-
def dump(data)
|
136
|
-
data || {}
|
137
|
-
end
|
138
|
-
|
139
|
-
end
|
140
|
-
```
|
126
|
+
If you want to use JSON column or Postgres HStore types, then you can pass in `ActiveRecord::TypedStore::IdentityCoder` as the coder.
|
141
127
|
|
142
128
|
## HStore limitations
|
143
129
|
|
@@ -148,7 +134,7 @@ Since HStore can only store strings:
|
|
148
134
|
- `any` attributes will be converted to string
|
149
135
|
|
150
136
|
If you use HStore because you need to be able to query the store from SQL, and any of these limitations are an issue for you,
|
151
|
-
|
137
|
+
then you could probably use the JSON column type, which do not suffer from these limitations and is also queriable.
|
152
138
|
|
153
139
|
## Contributing
|
154
140
|
|
@@ -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', '>= 5.2'
|
22
22
|
|
23
|
-
spec.add_development_dependency 'bundler'
|
23
|
+
spec.add_development_dependency 'bundler'
|
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
27
|
spec.add_development_dependency 'sqlite3', '~> 1'
|
28
|
-
spec.add_development_dependency 'pg', '~> 0.18'
|
28
|
+
spec.add_development_dependency 'pg', ENV.fetch('PG_VERSION', '~> 0.18')
|
29
29
|
spec.add_development_dependency 'mysql2', '> 0.3'
|
30
30
|
spec.add_development_dependency 'database_cleaner', '~> 1'
|
31
31
|
end
|
data/gemfiles/Gemfile.ar-5.0
CHANGED
@@ -1,11 +1,6 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
|
+
gemspec path: '..'
|
4
|
+
|
5
|
+
gem 'sqlite3', '~> 1.3.0'
|
3
6
|
gem 'activerecord', '~> 5.0.0'
|
4
|
-
gem 'bundler', '~> 1.3'
|
5
|
-
gem 'rake'
|
6
|
-
gem 'rspec'
|
7
|
-
gem 'sqlite3'
|
8
|
-
gem 'pg', '~> 0.11'
|
9
|
-
gem 'mysql2', ['>= 0.3.13', '< 0.5']
|
10
|
-
gem 'database_cleaner'
|
11
|
-
gem 'coveralls', require: false
|
data/gemfiles/Gemfile.ar-5.2
CHANGED
@@ -1,11 +1,5 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
gem '
|
6
|
-
gem 'rspec'
|
7
|
-
gem 'sqlite3'
|
8
|
-
gem 'pg', '~> 0.11'
|
9
|
-
gem 'mysql2', ['>= 0.3.13', '< 0.5']
|
10
|
-
gem 'database_cleaner'
|
11
|
-
gem 'coveralls', require: false
|
3
|
+
gemspec path: '..'
|
4
|
+
|
5
|
+
gem 'activerecord', '~> 5.2.0'
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support'
|
2
4
|
|
3
5
|
module ActiveRecord
|
@@ -7,5 +9,5 @@ end
|
|
7
9
|
|
8
10
|
ActiveSupport.on_load(:active_record) do
|
9
11
|
require 'active_record/typed_store/extension'
|
10
|
-
::ActiveRecord::Base.
|
12
|
+
::ActiveRecord::Base.extend(ActiveRecord::TypedStore::Extension)
|
11
13
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord::TypedStore
|
4
|
+
module Behavior
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def define_attribute_methods
|
9
|
+
super
|
10
|
+
define_typed_store_attribute_methods
|
11
|
+
end
|
12
|
+
|
13
|
+
def undefine_attribute_methods # :nodoc:
|
14
|
+
super if @typed_store_attribute_methods_generated
|
15
|
+
@typed_store_attribute_methods_generated = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def define_typed_store_attribute_methods
|
19
|
+
return if @typed_store_attribute_methods_generated
|
20
|
+
store_accessors.each do |attribute|
|
21
|
+
define_attribute_method(attribute)
|
22
|
+
undefine_before_type_cast_method(attribute)
|
23
|
+
end
|
24
|
+
@typed_store_attribute_methods_generated = true
|
25
|
+
end
|
26
|
+
|
27
|
+
def undefine_before_type_cast_method(attribute)
|
28
|
+
# because it mess with ActionView forms, see #14.
|
29
|
+
method = "#{attribute}_before_type_cast"
|
30
|
+
undef_method(method) if method_defined?(method)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def changes
|
35
|
+
changes = super
|
36
|
+
self.class.store_accessors.each do |attr|
|
37
|
+
if send("#{attr}_changed?")
|
38
|
+
changes[attr] = [send("#{attr}_was"), send(attr)]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
changes
|
42
|
+
end
|
43
|
+
|
44
|
+
def clear_attribute_change(attr_name)
|
45
|
+
return if self.class.store_accessors.include?(attr_name.to_s)
|
46
|
+
super
|
47
|
+
end
|
48
|
+
|
49
|
+
def read_attribute(attr_name)
|
50
|
+
if self.class.store_accessors.include?(attr_name.to_s)
|
51
|
+
return public_send(attr_name)
|
52
|
+
end
|
53
|
+
super
|
54
|
+
end
|
55
|
+
|
56
|
+
def attribute?(attr_name)
|
57
|
+
if self.class.store_accessors.include?(attr_name.to_s)
|
58
|
+
value = public_send(attr_name)
|
59
|
+
|
60
|
+
case value
|
61
|
+
when true then true
|
62
|
+
when false, nil then false
|
63
|
+
else
|
64
|
+
if value.respond_to?(:zero?)
|
65
|
+
!value.zero?
|
66
|
+
else
|
67
|
+
!value.blank?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
else
|
71
|
+
super
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_record/typed_store/field'
|
2
4
|
|
3
5
|
module ActiveRecord::TypedStore
|
@@ -29,7 +31,7 @@ module ActiveRecord::TypedStore
|
|
29
31
|
delegate :keys, to: :@fields
|
30
32
|
|
31
33
|
NO_DEFAULT_GIVEN = Object.new
|
32
|
-
[:string, :text, :integer, :float, :datetime, :date, :boolean, :decimal, :any].each do |type|
|
34
|
+
[:string, :text, :integer, :float, :time, :datetime, :date, :boolean, :decimal, :any].each do |type|
|
33
35
|
define_method(type) do |name, **options|
|
34
36
|
@fields[name] = Field.new(name, type, options)
|
35
37
|
end
|
@@ -1,122 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_record/typed_store/dsl'
|
4
|
+
require 'active_record/typed_store/behavior'
|
2
5
|
require 'active_record/typed_store/type'
|
3
6
|
require 'active_record/typed_store/typed_hash'
|
7
|
+
require 'active_record/typed_store/identity_coder'
|
4
8
|
|
5
9
|
module ActiveRecord::TypedStore
|
6
10
|
module Extension
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
self.typed_stores = {}
|
12
|
-
end
|
13
|
-
|
14
|
-
module ClassMethods
|
15
|
-
def store_accessors
|
16
|
-
typed_stores.each_value.flat_map(&:accessors)
|
11
|
+
def typed_store(store_attribute, options={}, &block)
|
12
|
+
unless self < Behavior
|
13
|
+
include Behavior
|
14
|
+
class_attribute :typed_stores, :store_accessors, instance_accessor: false
|
17
15
|
end
|
18
16
|
|
19
|
-
|
20
|
-
|
21
|
-
|
17
|
+
dsl = DSL.new(store_attribute, options, &block)
|
18
|
+
self.typed_stores = (self.typed_stores || {}).merge(store_attribute => dsl)
|
19
|
+
self.store_accessors = typed_stores.each_value.flat_map(&:accessors).map { |a| -a.to_s }.to_set
|
22
20
|
|
23
|
-
|
24
|
-
|
21
|
+
typed_klass = TypedHash.create(dsl.fields.values)
|
22
|
+
const_set("#{store_attribute}_hash".camelize, typed_klass)
|
25
23
|
|
26
|
-
|
24
|
+
if ActiveRecord.version >= Gem::Version.new('6.1.0.alpha')
|
25
|
+
attribute(store_attribute) do |subtype|
|
27
26
|
Type.new(typed_klass, dsl.coder, subtype)
|
28
27
|
end
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
define_method("#{accessor_name}_changed?") do
|
33
|
-
send("#{store_attribute}_changed?") &&
|
34
|
-
send(store_attribute)[accessor_name] != send("#{store_attribute}_was")[accessor_name]
|
35
|
-
end
|
36
|
-
|
37
|
-
define_method("#{accessor_name}_was") do
|
38
|
-
send("#{store_attribute}_was")[accessor_name]
|
39
|
-
end
|
40
|
-
|
41
|
-
define_method("restore_#{accessor_name}!") do
|
42
|
-
send("#{accessor_name}=", send("#{accessor_name}_was"))
|
43
|
-
end
|
28
|
+
else
|
29
|
+
decorate_attribute_type(store_attribute, :typed_store) do |subtype|
|
30
|
+
Type.new(typed_klass, dsl.coder, subtype)
|
44
31
|
end
|
45
32
|
end
|
33
|
+
store_accessor(store_attribute, dsl.accessors)
|
46
34
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
def undefine_attribute_methods # :nodoc:
|
53
|
-
super if @typed_store_attribute_methods_generated
|
54
|
-
@typed_store_attribute_methods_generated = false
|
55
|
-
end
|
56
|
-
|
57
|
-
def define_typed_store_attribute_methods
|
58
|
-
return if @typed_store_attribute_methods_generated
|
59
|
-
store_accessors.each do |attribute|
|
60
|
-
define_attribute_method(attribute.to_s)
|
61
|
-
undefine_before_type_cast_method(attribute)
|
35
|
+
dsl.accessors.each do |accessor_name|
|
36
|
+
define_method("#{accessor_name}_changed?") do
|
37
|
+
send("#{store_attribute}_changed?") &&
|
38
|
+
send(store_attribute)[accessor_name] != send("#{store_attribute}_was")[accessor_name]
|
62
39
|
end
|
63
|
-
@typed_store_attribute_methods_generated = true
|
64
|
-
end
|
65
40
|
|
66
|
-
|
67
|
-
|
68
|
-
method = "#{attribute}_before_type_cast"
|
69
|
-
undef_method(method) if method_defined?(method)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
def changes
|
74
|
-
changes = super
|
75
|
-
self.class.store_accessors.each do |attr|
|
76
|
-
if send("#{attr}_changed?")
|
77
|
-
changes[attr] = [send("#{attr}_was"), send(attr)]
|
41
|
+
define_method("#{accessor_name}_was") do
|
42
|
+
send("#{store_attribute}_was")[accessor_name]
|
78
43
|
end
|
79
|
-
end
|
80
|
-
changes
|
81
|
-
end
|
82
|
-
|
83
|
-
def clear_attribute_change(attr_name)
|
84
|
-
return if self.class.store_accessors.include?(normalize_attribute(attr_name))
|
85
|
-
super
|
86
|
-
end
|
87
|
-
|
88
|
-
def read_attribute(attr_name)
|
89
|
-
if self.class.store_accessors.include?(normalize_attribute(attr_name))
|
90
|
-
return public_send(attr_name)
|
91
|
-
end
|
92
|
-
super
|
93
|
-
end
|
94
44
|
|
95
|
-
|
96
|
-
|
97
|
-
value = public_send(attr_name)
|
98
|
-
|
99
|
-
case value
|
100
|
-
when true then true
|
101
|
-
when false, nil then false
|
102
|
-
else
|
103
|
-
if value.respond_to?(:zero?)
|
104
|
-
!value.zero?
|
105
|
-
else
|
106
|
-
!value.blank?
|
107
|
-
end
|
45
|
+
define_method("restore_#{accessor_name}!") do
|
46
|
+
send("#{accessor_name}=", send("#{accessor_name}_was"))
|
108
47
|
end
|
109
|
-
else
|
110
|
-
super
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
def normalize_attribute(attr)
|
115
|
-
case attr
|
116
|
-
when Symbol
|
117
|
-
attr
|
118
|
-
else
|
119
|
-
attr.to_s.to_sym
|
120
48
|
end
|
121
49
|
end
|
122
50
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActiveRecord::TypedStore
|
2
4
|
class Field
|
3
5
|
attr_reader :array, :blank, :name, :default, :type, :null, :accessor, :type_sym
|
@@ -39,6 +41,7 @@ module ActiveRecord::TypedStore
|
|
39
41
|
string: ::ActiveRecord::Type::String,
|
40
42
|
float: ::ActiveRecord::Type::Float,
|
41
43
|
date: ::ActiveRecord::Type::Date,
|
44
|
+
time: ::ActiveRecord::Type::Time,
|
42
45
|
datetime: ::ActiveRecord::Type::DateTime,
|
43
46
|
decimal: ::ActiveRecord::Type::Decimal,
|
44
47
|
any: ::ActiveRecord::Type::Value,
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActiveRecord::TypedStore
|
2
4
|
class TypedHash < HashWithIndifferentAccess
|
3
5
|
|
@@ -15,6 +17,9 @@ module ActiveRecord::TypedStore
|
|
15
17
|
end
|
16
18
|
end
|
17
19
|
|
20
|
+
delegate :with_indifferent_access, to: :to_h
|
21
|
+
delegate :slice, :except, :without, to: :with_indifferent_access
|
22
|
+
|
18
23
|
def initialize(constructor={})
|
19
24
|
super()
|
20
25
|
update(defaults_hash)
|
@@ -201,7 +201,27 @@ describe ActiveRecord::TypedStore::TypedHash do
|
|
201
201
|
hash.merge!(source: '')
|
202
202
|
expect(hash[:source]).to be == 'web'
|
203
203
|
end
|
204
|
+
|
205
|
+
end
|
206
|
+
|
207
|
+
describe '#except' do
|
208
|
+
|
209
|
+
it 'does not set the default for ignored keys' do
|
210
|
+
hash = hash_class.new(source: 'foo')
|
211
|
+
expect(hash.except(:source)).to_not have_key(:source)
|
212
|
+
end
|
213
|
+
|
204
214
|
end
|
215
|
+
|
216
|
+
describe '#slice' do
|
217
|
+
|
218
|
+
it 'does not set the default for ignored keys' do
|
219
|
+
hash = hash_class.new(source: 'foo')
|
220
|
+
expect(hash.slice(:not_source)).to_not have_key(:source)
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
|
205
225
|
end
|
206
226
|
|
207
227
|
context 'unknown columns' do
|
@@ -70,20 +70,11 @@ shared_examples 'any model' do
|
|
70
70
|
}.to change { !!model.age_changed? }.from(true).to(false)
|
71
71
|
end
|
72
72
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
}.to change { model.age }.from(24).to(12)
|
79
|
-
end
|
80
|
-
else
|
81
|
-
it 'can be reset individually' do
|
82
|
-
model.age = 24
|
83
|
-
expect {
|
84
|
-
model.reset_age!
|
85
|
-
}.to change { model.age }.from(24).to(12)
|
86
|
-
end
|
73
|
+
it 'can be restored individually' do
|
74
|
+
model.age = 24
|
75
|
+
expect {
|
76
|
+
model.restore_age!
|
77
|
+
}.to change { model.age }.from(24).to(12)
|
87
78
|
end
|
88
79
|
|
89
80
|
it 'does not dirty track assigning the same boolean' do
|
@@ -339,13 +330,13 @@ shared_examples 'any model' do
|
|
339
330
|
describe 'decimal attributes' do
|
340
331
|
|
341
332
|
it 'has the defined default as initial value' do
|
342
|
-
expect(model.total_price).to be == BigDecimal
|
333
|
+
expect(model.total_price).to be == BigDecimal('4.2')
|
343
334
|
expect(model.total_price).to be_a BigDecimal
|
344
335
|
end
|
345
336
|
|
346
337
|
it 'properly cast assigned value to decimal' do
|
347
338
|
model.shipping_cost = 4.2
|
348
|
-
expect(model.shipping_cost).to be == BigDecimal
|
339
|
+
expect(model.shipping_cost).to be == BigDecimal('4.2')
|
349
340
|
expect(model.shipping_cost).to be_a BigDecimal
|
350
341
|
end
|
351
342
|
|
@@ -357,7 +348,7 @@ shared_examples 'any model' do
|
|
357
348
|
|
358
349
|
it 'retreive a BigDecimal instance' do
|
359
350
|
model.update(shipping_cost: 4.2)
|
360
|
-
expect(model.reload.shipping_cost).to be == BigDecimal
|
351
|
+
expect(model.reload.shipping_cost).to be == BigDecimal('4.2')
|
361
352
|
expect(model.reload.shipping_cost).to be_a BigDecimal
|
362
353
|
end
|
363
354
|
|
@@ -367,17 +358,17 @@ shared_examples 'any model' do
|
|
367
358
|
end
|
368
359
|
|
369
360
|
it 'positive values are considered present' do
|
370
|
-
model.shipping_cost = BigDecimal
|
361
|
+
model.shipping_cost = BigDecimal('4.2')
|
371
362
|
expect(model.shipping_cost?).to be true
|
372
363
|
end
|
373
364
|
|
374
365
|
it 'negative values are considered present' do
|
375
|
-
model.shipping_cost = BigDecimal
|
366
|
+
model.shipping_cost = BigDecimal('-4.2')
|
376
367
|
expect(model.shipping_cost?).to be true
|
377
368
|
end
|
378
369
|
|
379
370
|
it '0 is not considered present' do
|
380
|
-
model.shipping_cost = BigDecimal
|
371
|
+
model.shipping_cost = BigDecimal('0')
|
381
372
|
expect(model.shipping_cost?).to be false
|
382
373
|
end
|
383
374
|
|
@@ -426,68 +417,64 @@ shared_examples 'any model' do
|
|
426
417
|
|
427
418
|
end
|
428
419
|
|
429
|
-
describe '
|
430
|
-
|
431
|
-
let(:
|
432
|
-
let(:
|
433
|
-
let(:time) { datetime_string.respond_to?(:in_time_zone) ? datetime_string.in_time_zone : Time.parse(datetime_string) }
|
420
|
+
describe 'time attributes' do
|
421
|
+
let(:time) { Time.new(1984, 6, 8, 13, 57, 12) }
|
422
|
+
let(:time_string) { '1984-06-08 13:57:12' }
|
423
|
+
let(:time) { time_string.respond_to?(:in_time_zone) ? time_string.in_time_zone : Time.parse(time_string) }
|
434
424
|
|
435
425
|
context "with ActiveRecord #{ActiveRecord::VERSION::STRING}" do
|
426
|
+
it 'has the defined default as initial value' do
|
427
|
+
model.save
|
428
|
+
expect(model.reload.published_at).to be == time
|
429
|
+
end
|
436
430
|
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
expect(model.published_at).to be == time
|
442
|
-
end
|
431
|
+
it 'retreive a time instance' do
|
432
|
+
model.update(published_at: time)
|
433
|
+
expect(model.reload.published_at).to be == time
|
434
|
+
end
|
443
435
|
|
436
|
+
if ActiveRecord::Base.time_zone_aware_attributes
|
444
437
|
it 'properly cast assigned value to time' do
|
445
|
-
model.remind_at =
|
438
|
+
model.remind_at = time_string
|
446
439
|
expect(model.remind_at).to be == time
|
447
440
|
end
|
448
|
-
|
449
|
-
it 'properly cast assigned value to time on save' do
|
450
|
-
model.remind_at = datetime_string
|
451
|
-
model.save
|
452
|
-
model.reload
|
453
|
-
expect(model.remind_at).to be == time
|
454
|
-
end
|
455
|
-
|
456
|
-
it 'retreive a Time instance' do
|
457
|
-
model.update(published_at: datetime)
|
458
|
-
expect(model.reload.published_at).to be == time
|
459
|
-
end
|
460
|
-
|
461
441
|
else
|
462
|
-
|
463
|
-
|
464
|
-
model.
|
465
|
-
expect(model.reload.published_at).to be == datetime
|
466
|
-
end
|
467
|
-
|
468
|
-
it 'retreive a DateTime instance' do
|
469
|
-
model.update(published_at: datetime)
|
470
|
-
expect(model.reload.published_at).to be == datetime
|
442
|
+
it 'properly cast assigned value to time' do
|
443
|
+
model.remind_at = time_string
|
444
|
+
expect(model.remind_at).to be == time
|
471
445
|
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
472
449
|
|
473
|
-
|
450
|
+
describe 'datetime attributes' do
|
474
451
|
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
end
|
452
|
+
let(:datetime) { DateTime.new(1984, 6, 8, 13, 57, 12) }
|
453
|
+
let(:datetime_string) { '1984-06-08 13:57:12' }
|
454
|
+
let(:time) { datetime_string.respond_to?(:in_time_zone) ? datetime_string.in_time_zone : Time.parse(datetime_string) }
|
479
455
|
|
480
|
-
|
456
|
+
context "with ActiveRecord #{ActiveRecord::VERSION::STRING}" do
|
457
|
+
it 'has the defined default as initial value' do
|
458
|
+
model.save
|
459
|
+
expect(model.reload.published_at).to be == datetime
|
460
|
+
end
|
481
461
|
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
462
|
+
it 'retreive a DateTime instance' do
|
463
|
+
model.update(published_at: datetime)
|
464
|
+
expect(model.reload.published_at).to be == datetime
|
465
|
+
end
|
486
466
|
|
467
|
+
if ActiveRecord::Base.time_zone_aware_attributes
|
468
|
+
it 'properly cast assigned value to time' do
|
469
|
+
model.remind_at = datetime_string
|
470
|
+
expect(model.remind_at).to be == time
|
471
|
+
end
|
472
|
+
else
|
473
|
+
it 'properly cast assigned value to datetime' do
|
474
|
+
model.remind_at = datetime_string
|
475
|
+
expect(model.remind_at).to be == datetime
|
487
476
|
end
|
488
|
-
|
489
477
|
end
|
490
|
-
|
491
478
|
end
|
492
479
|
|
493
480
|
it 'nillify unparsable datetimes' do
|
@@ -513,7 +500,7 @@ shared_examples 'any model' do
|
|
513
500
|
|
514
501
|
end
|
515
502
|
|
516
|
-
shared_examples 'a store' do |retain_type=true|
|
503
|
+
shared_examples 'a store' do |retain_type = true, settings_type = :text|
|
517
504
|
let(:model) { described_class.new }
|
518
505
|
|
519
506
|
describe "without connection" do
|
@@ -537,7 +524,7 @@ shared_examples 'a store' do |retain_type=true|
|
|
537
524
|
describe 'model.typed_stores' do
|
538
525
|
it "can access keys" do
|
539
526
|
stores = model.class.typed_stores
|
540
|
-
expect(stores[:settings].keys).to eq [:no_default, :name, :email, :cell_phone, :public, :enabled, :age, :max_length, :rate, :price, :published_on, :remind_on, :published_at, :remind_at, :total_price, :shipping_cost, :grades, :tags, :nickname, :author, :source, :signup, :country]
|
527
|
+
expect(stores[:settings].keys).to eq [:no_default, :name, :email, :cell_phone, :public, :enabled, :age, :max_length, :rate, :price, :published_on, :remind_on, :published_at_time, :remind_at_time, :published_at, :remind_at, :total_price, :shipping_cost, :grades, :tags, :nickname, :author, :source, :signup, :country]
|
541
528
|
end
|
542
529
|
|
543
530
|
it "can access keys even when accessors are not defined" do
|
@@ -617,7 +604,7 @@ shared_examples 'a store' do |retain_type=true|
|
|
617
604
|
end
|
618
605
|
|
619
606
|
it 'delegates internal methods to the underlying type' do
|
620
|
-
expect(model.class.type_for_attribute("settings").type).to eq
|
607
|
+
expect(model.class.type_for_attribute("settings").type).to eq settings_type
|
621
608
|
end
|
622
609
|
end
|
623
610
|
|
@@ -844,18 +831,9 @@ describe PostgresqlRegularARModel do
|
|
844
831
|
it_should_behave_like 'a model supporting arrays', true
|
845
832
|
end if defined?(PostgresqlRegularARModel)
|
846
833
|
|
847
|
-
describe PostgresHstoreTypedStoreModel do
|
848
|
-
if AR_VERSION >= AR_4_1
|
849
|
-
pending('TODO: Rails edge HStore compatibiliy')
|
850
|
-
else
|
851
|
-
it_should_behave_like 'any model'
|
852
|
-
it_should_behave_like 'a store', false
|
853
|
-
end
|
854
|
-
end if defined?(PostgresHstoreTypedStoreModel)
|
855
|
-
|
856
834
|
describe PostgresJsonTypedStoreModel do
|
857
835
|
it_should_behave_like 'any model'
|
858
|
-
it_should_behave_like 'a store'
|
836
|
+
it_should_behave_like 'a store', true, :json
|
859
837
|
it_should_behave_like 'a model supporting arrays'
|
860
838
|
end if defined?(PostgresJsonTypedStoreModel)
|
861
839
|
|
data/spec/support/models.rb
CHANGED
@@ -2,12 +2,6 @@ require 'active_record'
|
|
2
2
|
require 'json'
|
3
3
|
require 'yaml'
|
4
4
|
|
5
|
-
AR_VERSION = Gem::Version.new(ActiveRecord::VERSION::STRING)
|
6
|
-
AR_4_0 = Gem::Version.new('4.0')
|
7
|
-
AR_4_1 = Gem::Version.new('4.1.0.beta')
|
8
|
-
AR_4_2 = Gem::Version.new('4.2.0-rc1')
|
9
|
-
AR_5_0 = Gem::Version.new('5.0.0')
|
10
|
-
|
11
5
|
ActiveRecord::Base.time_zone_aware_attributes = ENV['TIMEZONE_AWARE'] != '0'
|
12
6
|
ActiveRecord::Base.configurations = {
|
13
7
|
'test_sqlite3' => { 'adapter' => 'sqlite3', 'database' => '/tmp/typed_store.db' },
|
@@ -34,6 +28,9 @@ def define_columns(t)
|
|
34
28
|
t.date :published_on, default: '1984-06-08', null: false
|
35
29
|
t.date :remind_on
|
36
30
|
|
31
|
+
t.time :published_at_time, default: '1984-06-08 13:57:12', null: false
|
32
|
+
t.time :remind_at_time
|
33
|
+
|
37
34
|
t.datetime :published_at, default: '1984-06-08 13:57:12', null: false
|
38
35
|
t.datetime :remind_at
|
39
36
|
|
@@ -77,7 +74,7 @@ def define_store_with_attributes(**options)
|
|
77
74
|
end
|
78
75
|
end
|
79
76
|
|
80
|
-
MigrationClass =
|
77
|
+
MigrationClass = ActiveRecord::Migration["5.0"]
|
81
78
|
class CreateAllTables < MigrationClass
|
82
79
|
|
83
80
|
def self.recreate_table(name, *args, &block)
|
@@ -95,14 +92,11 @@ class CreateAllTables < MigrationClass
|
|
95
92
|
ActiveRecord::Base.establish_connection(ENV['POSTGRES_URL'] || :test_postgresql)
|
96
93
|
recreate_table(:postgresql_regular_ar_models) { |t| define_columns(t); t.text :untyped_settings }
|
97
94
|
|
98
|
-
if
|
99
|
-
|
100
|
-
recreate_table(:postgres_hstore_typed_store_models) { |t| t.hstore :settings; t.text :untyped_settings }
|
95
|
+
execute "create extension if not exists hstore"
|
96
|
+
recreate_table(:postgres_hstore_typed_store_models) { |t| t.hstore :settings; t.text :untyped_settings }
|
101
97
|
|
102
|
-
|
103
|
-
|
104
|
-
recreate_table(:postgres_json_typed_store_models) { |t| t.json :settings; t.text :untyped_settings }
|
105
|
-
end
|
98
|
+
if ENV['POSTGRES_JSON']
|
99
|
+
recreate_table(:postgres_json_typed_store_models) { |t| t.json :settings; t.text :explicit_settings; t.text :partial_settings; t.text :untyped_settings }
|
106
100
|
end
|
107
101
|
end
|
108
102
|
|
@@ -161,9 +155,8 @@ if ENV['POSTGRES']
|
|
161
155
|
store :untyped_settings, accessors: [:title]
|
162
156
|
end
|
163
157
|
|
164
|
-
if
|
165
|
-
|
166
|
-
class PostgresHstoreTypedStoreModel < ActiveRecord::Base
|
158
|
+
if ENV['POSTGRES_JSON']
|
159
|
+
class PostgresJsonTypedStoreModel < ActiveRecord::Base
|
167
160
|
establish_connection ENV['POSTGRES_URL'] || :test_postgresql
|
168
161
|
store :untyped_settings, accessors: [:title]
|
169
162
|
|
@@ -171,35 +164,6 @@ if ENV['POSTGRES']
|
|
171
164
|
define_store_with_no_attributes(coder: ColumnCoder.new(AsJson))
|
172
165
|
define_store_with_partial_attributes(coder: ColumnCoder.new(AsJson))
|
173
166
|
end
|
174
|
-
|
175
|
-
if ENV['POSTGRES_JSON']
|
176
|
-
|
177
|
-
if AR_VERSION >= AR_4_2
|
178
|
-
|
179
|
-
class PostgresJsonTypedStoreModel < ActiveRecord::Base
|
180
|
-
establish_connection ENV['POSTGRES_URL'] || :test_postgresql
|
181
|
-
store :untyped_settings, accessors: [:title]
|
182
|
-
|
183
|
-
define_store_with_attributes(coder: false)
|
184
|
-
define_store_with_no_attributes(coder: false)
|
185
|
-
define_store_with_partial_attributes(coder: false)
|
186
|
-
end
|
187
|
-
|
188
|
-
else
|
189
|
-
|
190
|
-
class PostgresJsonTypedStoreModel < ActiveRecord::Base
|
191
|
-
establish_connection ENV['POSTGRES_URL'] || :test_postgresql
|
192
|
-
store :untyped_settings, accessors: [:title]
|
193
|
-
|
194
|
-
define_store_with_attributes(coder: ColumnCoder.new(AsJson))
|
195
|
-
define_store_with_no_attributes(coder: ColumnCoder.new(AsJson))
|
196
|
-
define_store_with_partial_attributes(coder: ColumnCoder.new(AsJson))
|
197
|
-
end
|
198
|
-
|
199
|
-
end
|
200
|
-
|
201
|
-
end
|
202
|
-
|
203
167
|
end
|
204
168
|
end
|
205
169
|
|
@@ -265,5 +229,4 @@ Models = [
|
|
265
229
|
]
|
266
230
|
Models << MysqlRegularARModel if defined?(MysqlRegularARModel)
|
267
231
|
Models << PostgresqlRegularARModel if defined?(PostgresqlRegularARModel)
|
268
|
-
Models << PostgresHstoreTypedStoreModel if defined?(PostgresHstoreTypedStoreModel)
|
269
232
|
Models << PostgresJsonTypedStoreModel if defined?(PostgresJsonTypedStoreModel)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-typedstore
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1
|
4
|
+
version: 1.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Boussier
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-07-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,34 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
20
|
-
- - "<"
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: '5.3'
|
19
|
+
version: '5.2'
|
23
20
|
type: :runtime
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
26
23
|
requirements:
|
27
24
|
- - ">="
|
28
25
|
- !ruby/object:Gem::Version
|
29
|
-
version: '
|
30
|
-
- - "<"
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '5.3'
|
26
|
+
version: '5.2'
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
28
|
name: bundler
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
36
30
|
requirements:
|
37
|
-
- - "
|
31
|
+
- - ">="
|
38
32
|
- !ruby/object:Gem::Version
|
39
|
-
version: '
|
33
|
+
version: '0'
|
40
34
|
type: :development
|
41
35
|
prerelease: false
|
42
36
|
version_requirements: !ruby/object:Gem::Requirement
|
43
37
|
requirements:
|
44
|
-
- - "
|
38
|
+
- - ">="
|
45
39
|
- !ruby/object:Gem::Version
|
46
|
-
version: '
|
40
|
+
version: '0'
|
47
41
|
- !ruby/object:Gem::Dependency
|
48
42
|
name: rake
|
49
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -157,14 +151,16 @@ files:
|
|
157
151
|
- README.md
|
158
152
|
- Rakefile
|
159
153
|
- activerecord-typedstore.gemspec
|
160
|
-
- gemfiles/Gemfile.ar-4.2
|
161
154
|
- gemfiles/Gemfile.ar-5.0
|
162
|
-
- gemfiles/Gemfile.ar-5.1
|
163
155
|
- gemfiles/Gemfile.ar-5.2
|
156
|
+
- gemfiles/Gemfile.ar-6.0
|
157
|
+
- gemfiles/Gemfile.ar-master
|
164
158
|
- lib/active_record/typed_store.rb
|
159
|
+
- lib/active_record/typed_store/behavior.rb
|
165
160
|
- lib/active_record/typed_store/dsl.rb
|
166
161
|
- lib/active_record/typed_store/extension.rb
|
167
162
|
- lib/active_record/typed_store/field.rb
|
163
|
+
- lib/active_record/typed_store/identity_coder.rb
|
168
164
|
- lib/active_record/typed_store/type.rb
|
169
165
|
- lib/active_record/typed_store/typed_hash.rb
|
170
166
|
- lib/active_record/typed_store/version.rb
|
@@ -178,7 +174,7 @@ homepage: https://github.com/byroot/activerecord-typedstore
|
|
178
174
|
licenses:
|
179
175
|
- MIT
|
180
176
|
metadata: {}
|
181
|
-
post_install_message:
|
177
|
+
post_install_message:
|
182
178
|
rdoc_options: []
|
183
179
|
require_paths:
|
184
180
|
- lib
|
@@ -193,9 +189,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
193
189
|
- !ruby/object:Gem::Version
|
194
190
|
version: '0'
|
195
191
|
requirements: []
|
196
|
-
|
197
|
-
|
198
|
-
signing_key:
|
192
|
+
rubygems_version: 3.1.2
|
193
|
+
signing_key:
|
199
194
|
specification_version: 4
|
200
195
|
summary: Add type casting and full method attributes support to АctiveRecord store
|
201
196
|
test_files:
|
data/gemfiles/Gemfile.ar-4.2
DELETED
data/gemfiles/Gemfile.ar-5.1
DELETED