activerecord-typedstore 1.2.2 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/ruby.yml +47 -0
- data/activerecord-typedstore.gemspec +2 -2
- data/gemfiles/{Gemfile.ar-5.1 → Gemfile.ar-6.0} +1 -1
- data/gemfiles/Gemfile.ar-6.1 +6 -0
- data/gemfiles/Gemfile.ar-master +1 -0
- data/lib/active_record/typed_store/behavior.rb +75 -0
- data/lib/active_record/typed_store/dsl.rb +2 -0
- data/lib/active_record/typed_store/extension.rb +27 -99
- data/lib/active_record/typed_store/field.rb +2 -0
- data/lib/active_record/typed_store/identity_coder.rb +2 -0
- data/lib/active_record/typed_store/type.rb +4 -2
- data/lib/active_record/typed_store/typed_hash.rb +2 -0
- data/lib/active_record/typed_store/version.rb +3 -1
- data/lib/active_record/typed_store.rb +3 -1
- data/lib/activerecord-typedstore.rb +2 -0
- data/spec/active_record/typed_store_spec.rb +59 -109
- data/spec/support/models.rb +8 -47
- metadata +12 -12
- data/.travis.yml +0 -40
- data/gemfiles/Gemfile.ar-4.2 +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 17f01f8701f6af429d7fb15bfbcf9cf202f1d82cbe0f6878aa4a503ed69fc0e8
|
4
|
+
data.tar.gz: e5e1dd4100a034b074fe2682e485ae1365c8bcbe9c4bb7bc96f697a5042c0f62
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f1703f9023ab91ac749d74335276a0824d3072060eac8a68270f2b52e41198dfeebecffd54738cd4b097fe97948371479e2db1c1fa8ebc30ca07eeeaf63bac91
|
7
|
+
data.tar.gz: 8e48b1f137a374053092ccf52f3411759fcf421d1e4d8cf8b7f47b3c25d935aaf958f568ee35423381171aca879bf68a7b943493cf4947a8d3af19f66d54616f
|
@@ -0,0 +1,47 @@
|
|
1
|
+
name: Ruby
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
name: Ruby ${{ matrix.ruby }}
|
9
|
+
strategy:
|
10
|
+
matrix:
|
11
|
+
ruby: [2.6, 2.7]
|
12
|
+
gemfile: [Gemfile.ar-5.2, Gemfile.ar-6.0, Gemfile.ar-6.1, Gemfile.ar-master]
|
13
|
+
exclude:
|
14
|
+
- ruby: 2.6
|
15
|
+
gemfile: Gemfile.ar-6.0
|
16
|
+
- ruby: 2.6
|
17
|
+
gemfile: Gemfile.ar-6.1
|
18
|
+
- ruby: 2.6
|
19
|
+
gemfile: Gemfile.ar-master
|
20
|
+
timezone_aware: [0, 1]
|
21
|
+
env:
|
22
|
+
BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}
|
23
|
+
TIMEZONE_AWARE: ${{ matrix.timezone_aware }}
|
24
|
+
POSTGRES: 1
|
25
|
+
MYSQL: 1
|
26
|
+
POSTGRES_JSON: 1
|
27
|
+
steps:
|
28
|
+
- name: Check out code
|
29
|
+
uses: actions/checkout@v2
|
30
|
+
- name: Set up Ruby ${{ matrix.ruby }}
|
31
|
+
uses: ruby/setup-ruby@v1
|
32
|
+
with:
|
33
|
+
ruby-version: ${{ matrix.ruby }}
|
34
|
+
bundler-cache: true
|
35
|
+
- name: Start MySQL and create DB
|
36
|
+
run: |
|
37
|
+
sudo systemctl start mysql.service
|
38
|
+
mysql -uroot -proot -e 'create database typed_store_test;'
|
39
|
+
- name: Start PostgresQL and create DB
|
40
|
+
run: |
|
41
|
+
sudo sed -i s/md5/trust/g /etc/postgresql/*/main/pg_hba.conf
|
42
|
+
sudo systemctl start postgresql.service
|
43
|
+
sudo -u postgres createuser --createdb --superuser ${USER}
|
44
|
+
createdb typed_store_test
|
45
|
+
echo "POSTGRES_URL=postgres://localhost/typed_store_test" >> $GITHUB_ENV
|
46
|
+
- name: Ruby Tests
|
47
|
+
run: bundle exec rake
|
@@ -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
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-master
CHANGED
@@ -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,123 +1,51 @@
|
|
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'
|
4
7
|
require 'active_record/typed_store/identity_coder'
|
5
8
|
|
6
9
|
module ActiveRecord::TypedStore
|
7
10
|
module Extension
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
self.typed_stores = {}
|
13
|
-
end
|
14
|
-
|
15
|
-
module ClassMethods
|
16
|
-
def store_accessors
|
17
|
-
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
|
18
15
|
end
|
19
16
|
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
23
20
|
|
24
|
-
|
25
|
-
|
21
|
+
typed_klass = TypedHash.create(dsl.fields.values)
|
22
|
+
const_set("#{store_attribute}_hash".camelize, typed_klass)
|
26
23
|
|
27
|
-
|
24
|
+
if ActiveRecord.version >= Gem::Version.new('6.1.0.alpha')
|
25
|
+
attribute(store_attribute) do |subtype|
|
26
|
+
subtype = subtype.subtype if subtype.is_a?(Type)
|
28
27
|
Type.new(typed_klass, dsl.coder, subtype)
|
29
28
|
end
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
define_method("#{accessor_name}_changed?") do
|
34
|
-
send("#{store_attribute}_changed?") &&
|
35
|
-
send(store_attribute)[accessor_name] != send("#{store_attribute}_was")[accessor_name]
|
36
|
-
end
|
37
|
-
|
38
|
-
define_method("#{accessor_name}_was") do
|
39
|
-
send("#{store_attribute}_was")[accessor_name]
|
40
|
-
end
|
41
|
-
|
42
|
-
define_method("restore_#{accessor_name}!") do
|
43
|
-
send("#{accessor_name}=", send("#{accessor_name}_was"))
|
44
|
-
end
|
29
|
+
else
|
30
|
+
decorate_attribute_type(store_attribute, :typed_store) do |subtype|
|
31
|
+
Type.new(typed_klass, dsl.coder, subtype)
|
45
32
|
end
|
46
33
|
end
|
34
|
+
store_accessor(store_attribute, dsl.accessors)
|
47
35
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
def undefine_attribute_methods # :nodoc:
|
54
|
-
super if @typed_store_attribute_methods_generated
|
55
|
-
@typed_store_attribute_methods_generated = false
|
56
|
-
end
|
57
|
-
|
58
|
-
def define_typed_store_attribute_methods
|
59
|
-
return if @typed_store_attribute_methods_generated
|
60
|
-
store_accessors.each do |attribute|
|
61
|
-
define_attribute_method(attribute.to_s)
|
62
|
-
undefine_before_type_cast_method(attribute)
|
36
|
+
dsl.accessors.each do |accessor_name|
|
37
|
+
define_method("#{accessor_name}_changed?") do
|
38
|
+
send("#{store_attribute}_changed?") &&
|
39
|
+
send(store_attribute)[accessor_name] != send("#{store_attribute}_was")[accessor_name]
|
63
40
|
end
|
64
|
-
@typed_store_attribute_methods_generated = true
|
65
|
-
end
|
66
41
|
|
67
|
-
|
68
|
-
|
69
|
-
method = "#{attribute}_before_type_cast"
|
70
|
-
undef_method(method) if method_defined?(method)
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def changes
|
75
|
-
changes = super
|
76
|
-
self.class.store_accessors.each do |attr|
|
77
|
-
if send("#{attr}_changed?")
|
78
|
-
changes[attr] = [send("#{attr}_was"), send(attr)]
|
42
|
+
define_method("#{accessor_name}_was") do
|
43
|
+
send("#{store_attribute}_was")[accessor_name]
|
79
44
|
end
|
80
|
-
end
|
81
|
-
changes
|
82
|
-
end
|
83
|
-
|
84
|
-
def clear_attribute_change(attr_name)
|
85
|
-
return if self.class.store_accessors.include?(normalize_attribute(attr_name))
|
86
|
-
super
|
87
|
-
end
|
88
|
-
|
89
|
-
def read_attribute(attr_name)
|
90
|
-
if self.class.store_accessors.include?(normalize_attribute(attr_name))
|
91
|
-
return public_send(attr_name)
|
92
|
-
end
|
93
|
-
super
|
94
|
-
end
|
95
45
|
|
96
|
-
|
97
|
-
|
98
|
-
value = public_send(attr_name)
|
99
|
-
|
100
|
-
case value
|
101
|
-
when true then true
|
102
|
-
when false, nil then false
|
103
|
-
else
|
104
|
-
if value.respond_to?(:zero?)
|
105
|
-
!value.zero?
|
106
|
-
else
|
107
|
-
!value.blank?
|
108
|
-
end
|
46
|
+
define_method("restore_#{accessor_name}!") do
|
47
|
+
send("#{accessor_name}=", send("#{accessor_name}_was"))
|
109
48
|
end
|
110
|
-
else
|
111
|
-
super
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
def normalize_attribute(attr)
|
116
|
-
case attr
|
117
|
-
when Symbol
|
118
|
-
attr
|
119
|
-
else
|
120
|
-
attr.to_s.to_sym
|
121
49
|
end
|
122
50
|
end
|
123
51
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActiveRecord::TypedStore
|
2
4
|
class Type < ActiveRecord::Type::Serialized
|
3
5
|
def initialize(typed_hash_klass, coder, subtype)
|
@@ -21,8 +23,8 @@ module ActiveRecord::TypedStore
|
|
21
23
|
define_method(method) do |value|
|
22
24
|
return if value.nil?
|
23
25
|
|
24
|
-
if value.respond_to?(:
|
25
|
-
super(value.
|
26
|
+
if value.respond_to?(:to_hash)
|
27
|
+
super(value.to_hash)
|
26
28
|
else
|
27
29
|
raise ArgumentError, "ActiveRecord::TypedStore expects a hash as a column value, #{value.class} received"
|
28
30
|
end
|
@@ -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
|
@@ -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
|
|
@@ -432,48 +423,26 @@ shared_examples 'any model' do
|
|
432
423
|
let(:time) { time_string.respond_to?(:in_time_zone) ? time_string.in_time_zone : Time.parse(time_string) }
|
433
424
|
|
434
425
|
context "with ActiveRecord #{ActiveRecord::VERSION::STRING}" do
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
426
|
+
it 'has the defined default as initial value' do
|
427
|
+
model.save
|
428
|
+
expect(model.reload.published_at).to be == time
|
429
|
+
end
|
430
|
+
|
431
|
+
it 'retreive a time instance' do
|
432
|
+
model.update(published_at: time)
|
433
|
+
expect(model.reload.published_at).to be == time
|
434
|
+
end
|
440
435
|
|
436
|
+
if ActiveRecord::Base.time_zone_aware_attributes
|
441
437
|
it 'properly cast assigned value to time' do
|
442
438
|
model.remind_at = time_string
|
443
439
|
expect(model.remind_at).to be == time
|
444
440
|
end
|
445
|
-
|
441
|
+
else
|
442
|
+
it 'properly cast assigned value to time' do
|
446
443
|
model.remind_at = time_string
|
447
|
-
model.save
|
448
|
-
model.reload
|
449
444
|
expect(model.remind_at).to be == time
|
450
445
|
end
|
451
|
-
it 'retreive a Time instance' do
|
452
|
-
model.update(published_at: time)
|
453
|
-
expect(model.reload.published_at).to be == time
|
454
|
-
end
|
455
|
-
else
|
456
|
-
it 'has the defined default as initial value' do
|
457
|
-
model.save
|
458
|
-
expect(model.reload.published_at).to be == time
|
459
|
-
end
|
460
|
-
|
461
|
-
it 'retreive a time instance' do
|
462
|
-
model.update(published_at: time)
|
463
|
-
expect(model.reload.published_at).to be == time
|
464
|
-
end
|
465
|
-
|
466
|
-
if ActiveRecord::Base.time_zone_aware_attributes
|
467
|
-
it 'properly cast assigned value to time' do
|
468
|
-
model.remind_at = time_string
|
469
|
-
expect(model.remind_at).to be == time
|
470
|
-
end
|
471
|
-
else
|
472
|
-
it 'properly cast assigned value to time' do
|
473
|
-
model.remind_at = time_string
|
474
|
-
expect(model.remind_at).to be == time
|
475
|
-
end
|
476
|
-
end
|
477
446
|
end
|
478
447
|
end
|
479
448
|
end
|
@@ -485,61 +454,27 @@ shared_examples 'any model' do
|
|
485
454
|
let(:time) { datetime_string.respond_to?(:in_time_zone) ? datetime_string.in_time_zone : Time.parse(datetime_string) }
|
486
455
|
|
487
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
|
488
461
|
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
expect(model.published_at).to be == time
|
494
|
-
end
|
462
|
+
it 'retreive a DateTime instance' do
|
463
|
+
model.update(published_at: datetime)
|
464
|
+
expect(model.reload.published_at).to be == datetime
|
465
|
+
end
|
495
466
|
|
467
|
+
if ActiveRecord::Base.time_zone_aware_attributes
|
496
468
|
it 'properly cast assigned value to time' do
|
497
469
|
model.remind_at = datetime_string
|
498
470
|
expect(model.remind_at).to be == time
|
499
471
|
end
|
500
|
-
|
501
|
-
it 'properly cast assigned value to time on save' do
|
502
|
-
model.remind_at = datetime_string
|
503
|
-
model.save
|
504
|
-
model.reload
|
505
|
-
expect(model.remind_at).to be == time
|
506
|
-
end
|
507
|
-
|
508
|
-
it 'retreive a Time instance' do
|
509
|
-
model.update(published_at: datetime)
|
510
|
-
expect(model.reload.published_at).to be == time
|
511
|
-
end
|
512
|
-
|
513
472
|
else
|
514
|
-
|
515
|
-
|
516
|
-
model.
|
517
|
-
expect(model.reload.published_at).to be == datetime
|
518
|
-
end
|
519
|
-
|
520
|
-
it 'retreive a DateTime instance' do
|
521
|
-
model.update(published_at: datetime)
|
522
|
-
expect(model.reload.published_at).to be == datetime
|
523
|
-
end
|
524
|
-
|
525
|
-
if ActiveRecord::Base.time_zone_aware_attributes
|
526
|
-
|
527
|
-
it 'properly cast assigned value to time' do
|
528
|
-
model.remind_at = datetime_string
|
529
|
-
expect(model.remind_at).to be == time
|
530
|
-
end
|
531
|
-
|
532
|
-
else
|
533
|
-
|
534
|
-
it 'properly cast assigned value to datetime' do
|
535
|
-
model.remind_at = datetime_string
|
536
|
-
expect(model.remind_at).to be == datetime
|
537
|
-
end
|
538
|
-
|
473
|
+
it 'properly cast assigned value to datetime' do
|
474
|
+
model.remind_at = datetime_string
|
475
|
+
expect(model.remind_at).to be == datetime
|
539
476
|
end
|
540
|
-
|
541
477
|
end
|
542
|
-
|
543
478
|
end
|
544
479
|
|
545
480
|
it 'nillify unparsable datetimes' do
|
@@ -788,6 +723,11 @@ shared_examples 'a store' do |retain_type = true, settings_type = :text|
|
|
788
723
|
expect(model.explicit_settings[:signup][:counter]).to be_nil
|
789
724
|
end
|
790
725
|
|
726
|
+
it 'coerce hashes to HashWithIndifferentAccess' do # this is actually Rails behavior
|
727
|
+
model.signup[:metadata] = { "signed_up_at" => Time.now }
|
728
|
+
expect(model.signup[:metadata]).to be_a ActiveSupport::HashWithIndifferentAccess
|
729
|
+
end
|
730
|
+
|
791
731
|
end
|
792
732
|
end
|
793
733
|
|
@@ -896,15 +836,6 @@ describe PostgresqlRegularARModel do
|
|
896
836
|
it_should_behave_like 'a model supporting arrays', true
|
897
837
|
end if defined?(PostgresqlRegularARModel)
|
898
838
|
|
899
|
-
describe PostgresHstoreTypedStoreModel do
|
900
|
-
if AR_VERSION >= AR_4_1
|
901
|
-
pending('TODO: Rails edge HStore compatibiliy')
|
902
|
-
else
|
903
|
-
it_should_behave_like 'any model'
|
904
|
-
it_should_behave_like 'a store', false
|
905
|
-
end
|
906
|
-
end if defined?(PostgresHstoreTypedStoreModel)
|
907
|
-
|
908
839
|
describe PostgresJsonTypedStoreModel do
|
909
840
|
it_should_behave_like 'any model'
|
910
841
|
it_should_behave_like 'a store', true, :json
|
@@ -915,6 +846,11 @@ describe YamlTypedStoreModel do
|
|
915
846
|
it_should_behave_like 'any model'
|
916
847
|
it_should_behave_like 'a store'
|
917
848
|
it_should_behave_like 'a model supporting arrays'
|
849
|
+
|
850
|
+
it 'nested hashes are not serialized as HashWithIndifferentAccess' do
|
851
|
+
model = described_class.create!
|
852
|
+
expect(model.settings_before_type_cast).not_to include('HashWithIndifferentAccess')
|
853
|
+
end
|
918
854
|
end
|
919
855
|
|
920
856
|
describe JsonTypedStoreModel do
|
@@ -928,3 +864,17 @@ describe MarshalTypedStoreModel do
|
|
928
864
|
it_should_behave_like 'a store'
|
929
865
|
it_should_behave_like 'a model supporting arrays'
|
930
866
|
end
|
867
|
+
|
868
|
+
describe InheritedTypedStoreModel do
|
869
|
+
let(:model) { described_class.new }
|
870
|
+
|
871
|
+
it 'can be serialized' do
|
872
|
+
model.update(new_attribute: "foobar")
|
873
|
+
expect(model.reload.new_attribute).to be == "foobar"
|
874
|
+
end
|
875
|
+
|
876
|
+
it 'is casted' do
|
877
|
+
model.update(new_attribute: 42)
|
878
|
+
expect(model.settings[:new_attribute]).to be == '42'
|
879
|
+
end
|
880
|
+
end
|
data/spec/support/models.rb
CHANGED
@@ -2,17 +2,11 @@ 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' },
|
14
8
|
'test_postgresql' => { 'adapter' => 'postgresql', 'database' => 'typed_store_test', 'username' => 'postgres' },
|
15
|
-
'test_mysql' => { 'adapter' => 'mysql2', 'database' => 'typed_store_test', 'username' => '
|
9
|
+
'test_mysql' => { 'adapter' => 'mysql2', 'database' => 'typed_store_test', 'username' => 'root', 'password' => 'root' },
|
16
10
|
}
|
17
11
|
|
18
12
|
def define_columns(t)
|
@@ -80,7 +74,7 @@ def define_store_with_attributes(**options)
|
|
80
74
|
end
|
81
75
|
end
|
82
76
|
|
83
|
-
MigrationClass =
|
77
|
+
MigrationClass = ActiveRecord::Migration["5.0"]
|
84
78
|
class CreateAllTables < MigrationClass
|
85
79
|
|
86
80
|
def self.recreate_table(name, *args, &block)
|
@@ -98,13 +92,11 @@ class CreateAllTables < MigrationClass
|
|
98
92
|
ActiveRecord::Base.establish_connection(ENV['POSTGRES_URL'] || :test_postgresql)
|
99
93
|
recreate_table(:postgresql_regular_ar_models) { |t| define_columns(t); t.text :untyped_settings }
|
100
94
|
|
101
|
-
if
|
102
|
-
|
103
|
-
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 }
|
104
97
|
|
105
|
-
|
106
|
-
|
107
|
-
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 }
|
108
100
|
end
|
109
101
|
end
|
110
102
|
|
@@ -163,9 +155,8 @@ if ENV['POSTGRES']
|
|
163
155
|
store :untyped_settings, accessors: [:title]
|
164
156
|
end
|
165
157
|
|
166
|
-
if
|
167
|
-
|
168
|
-
class PostgresHstoreTypedStoreModel < ActiveRecord::Base
|
158
|
+
if ENV['POSTGRES_JSON']
|
159
|
+
class PostgresJsonTypedStoreModel < ActiveRecord::Base
|
169
160
|
establish_connection ENV['POSTGRES_URL'] || :test_postgresql
|
170
161
|
store :untyped_settings, accessors: [:title]
|
171
162
|
|
@@ -173,35 +164,6 @@ if ENV['POSTGRES']
|
|
173
164
|
define_store_with_no_attributes(coder: ColumnCoder.new(AsJson))
|
174
165
|
define_store_with_partial_attributes(coder: ColumnCoder.new(AsJson))
|
175
166
|
end
|
176
|
-
|
177
|
-
if ENV['POSTGRES_JSON']
|
178
|
-
|
179
|
-
if AR_VERSION >= AR_4_2
|
180
|
-
|
181
|
-
class PostgresJsonTypedStoreModel < ActiveRecord::Base
|
182
|
-
establish_connection ENV['POSTGRES_URL'] || :test_postgresql
|
183
|
-
store :untyped_settings, accessors: [:title]
|
184
|
-
|
185
|
-
define_store_with_attributes(coder: ColumnCoder.new(AsJson))
|
186
|
-
define_store_with_no_attributes(coder: ColumnCoder.new(AsJson))
|
187
|
-
define_store_with_partial_attributes(coder: ColumnCoder.new(AsJson))
|
188
|
-
end
|
189
|
-
|
190
|
-
else
|
191
|
-
|
192
|
-
class PostgresJsonTypedStoreModel < ActiveRecord::Base
|
193
|
-
establish_connection ENV['POSTGRES_URL'] || :test_postgresql
|
194
|
-
store :untyped_settings, accessors: [:title]
|
195
|
-
|
196
|
-
define_store_with_attributes(coder: ColumnCoder.new(AsJson))
|
197
|
-
define_store_with_no_attributes(coder: ColumnCoder.new(AsJson))
|
198
|
-
define_store_with_partial_attributes(coder: ColumnCoder.new(AsJson))
|
199
|
-
end
|
200
|
-
|
201
|
-
end
|
202
|
-
|
203
|
-
end
|
204
|
-
|
205
167
|
end
|
206
168
|
end
|
207
169
|
|
@@ -267,5 +229,4 @@ Models = [
|
|
267
229
|
]
|
268
230
|
Models << MysqlRegularARModel if defined?(MysqlRegularARModel)
|
269
231
|
Models << PostgresqlRegularARModel if defined?(PostgresqlRegularARModel)
|
270
|
-
Models << PostgresHstoreTypedStoreModel if defined?(PostgresHstoreTypedStoreModel)
|
271
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.
|
4
|
+
version: 1.4.0
|
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: 2021-09-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '5.2'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '5.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -143,20 +143,21 @@ executables: []
|
|
143
143
|
extensions: []
|
144
144
|
extra_rdoc_files: []
|
145
145
|
files:
|
146
|
+
- ".github/workflows/ruby.yml"
|
146
147
|
- ".gitignore"
|
147
148
|
- ".rspec"
|
148
|
-
- ".travis.yml"
|
149
149
|
- Gemfile
|
150
150
|
- LICENSE.txt
|
151
151
|
- README.md
|
152
152
|
- Rakefile
|
153
153
|
- activerecord-typedstore.gemspec
|
154
|
-
- gemfiles/Gemfile.ar-4.2
|
155
154
|
- gemfiles/Gemfile.ar-5.0
|
156
|
-
- gemfiles/Gemfile.ar-5.1
|
157
155
|
- gemfiles/Gemfile.ar-5.2
|
156
|
+
- gemfiles/Gemfile.ar-6.0
|
157
|
+
- gemfiles/Gemfile.ar-6.1
|
158
158
|
- gemfiles/Gemfile.ar-master
|
159
159
|
- lib/active_record/typed_store.rb
|
160
|
+
- lib/active_record/typed_store/behavior.rb
|
160
161
|
- lib/active_record/typed_store/dsl.rb
|
161
162
|
- lib/active_record/typed_store/extension.rb
|
162
163
|
- lib/active_record/typed_store/field.rb
|
@@ -174,7 +175,7 @@ homepage: https://github.com/byroot/activerecord-typedstore
|
|
174
175
|
licenses:
|
175
176
|
- MIT
|
176
177
|
metadata: {}
|
177
|
-
post_install_message:
|
178
|
+
post_install_message:
|
178
179
|
rdoc_options: []
|
179
180
|
require_paths:
|
180
181
|
- lib
|
@@ -189,9 +190,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
189
190
|
- !ruby/object:Gem::Version
|
190
191
|
version: '0'
|
191
192
|
requirements: []
|
192
|
-
|
193
|
-
|
194
|
-
signing_key:
|
193
|
+
rubygems_version: 3.1.2
|
194
|
+
signing_key:
|
195
195
|
specification_version: 4
|
196
196
|
summary: Add type casting and full method attributes support to АctiveRecord store
|
197
197
|
test_files:
|
data/.travis.yml
DELETED
@@ -1,40 +0,0 @@
|
|
1
|
-
language: ruby
|
2
|
-
|
3
|
-
before_install:
|
4
|
-
- gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
|
5
|
-
- gem install bundler -v '< 2'
|
6
|
-
|
7
|
-
rvm:
|
8
|
-
- 2.3.8
|
9
|
-
- 2.4.6
|
10
|
-
- 2.5.5
|
11
|
-
- 2.6.2
|
12
|
-
gemfile:
|
13
|
-
- gemfiles/Gemfile.ar-4.2
|
14
|
-
- gemfiles/Gemfile.ar-5.0
|
15
|
-
- gemfiles/Gemfile.ar-5.1
|
16
|
-
- gemfiles/Gemfile.ar-5.2
|
17
|
-
- gemfiles/Gemfile.ar-master
|
18
|
-
|
19
|
-
env:
|
20
|
-
- TIMEZONE_AWARE=1 POSTGRES=1 MYSQL=1
|
21
|
-
- TIMEZONE_AWARE=0 POSTGRES=1 MYSQL=1
|
22
|
-
- TIMEZONE_AWARE=1 POSTGRES=1 POSTGRES_JSON=1
|
23
|
-
- TIMEZONE_AWARE=0 POSTGRES=1 POSTGRES_JSON=1
|
24
|
-
|
25
|
-
matrix:
|
26
|
-
exclude:
|
27
|
-
- rvm: 2.3.8
|
28
|
-
gemfile: 'gemfiles/Gemfile.ar-master'
|
29
|
-
- rvm: 2.4.6
|
30
|
-
gemfile: 'gemfiles/Gemfile.ar-master'
|
31
|
-
|
32
|
-
addons:
|
33
|
-
postgresql: 9.3
|
34
|
-
|
35
|
-
sudo: false
|
36
|
-
cache: bundler
|
37
|
-
|
38
|
-
before_script:
|
39
|
-
- mysql -e 'create database typed_store_test;'
|
40
|
-
- psql -c 'create database typed_store_test;' -U postgres
|