activerecord-typedstore 1.1.3 → 1.3.1
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 +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