store_attribute 0.5.3 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +8 -3
- data/lib/store_attribute/active_record/type/typed_store.rb +37 -17
- data/lib/store_attribute/version.rb +1 -1
- data/spec/cases/store_attribute_spec.rb +44 -1
- data/spec/store_attribute/typed_store_spec.rb +131 -0
- data/spec/support/user.rb +6 -1
- data/store_attribute.gemspec +8 -0
- metadata +10 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 44ef41ba6670ce7ce109d0a856948ba3ae113328037e9b52025b7a7229b6580d
|
4
|
+
data.tar.gz: e25ade087cdad2fe69394462b227a0c1b580b4a0d977a8f505c6e28af916e143
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 29f7c336831838593456099308e7549e41a6d879585c1fb6f92dc2e48ba5150c4bba6856b2b9cb6e680c0b02882d8015f76d16bfcabab2ab73476d7e7dc4bc47
|
7
|
+
data.tar.gz: 0ca99dc3856dfef871cf57729bef285118e2a775b9dbb4b64038d39c2f6480aa9d6c162e97d54ffa7ea5d0d100cd3fccc016658268e76dcba6bed3d2ef7e0fa5
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Change log
|
2
|
+
|
3
|
+
## master
|
4
|
+
|
5
|
+
## 0.6.0 (2019-07-24)
|
6
|
+
|
7
|
+
- Added default values support. ([@dreikanter][], [@SumLare][])
|
8
|
+
|
9
|
+
See [PR #7](https://github.com/palkan/store_attribute/pull/7).
|
10
|
+
|
11
|
+
- Start keeping changelog. ([@palkan][])
|
12
|
+
|
13
|
+
[@palkan]: https://github.com/palkan
|
14
|
+
[@dreikanter]: https://github.com/dreikanter
|
15
|
+
[@SumLare]: https://github.com/SumLare
|
data/README.md
CHANGED
@@ -6,8 +6,7 @@ ActiveRecord extension which adds typecasting to store accessors.
|
|
6
6
|
|
7
7
|
Compatible with Rails 4.2 and Rails 5+.
|
8
8
|
|
9
|
-
|
10
|
-
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
|
9
|
+
Extracted from not merged PR to Rails: [rails/rails#18942](https://github.com/rails/rails/pull/18942).
|
11
10
|
|
12
11
|
### Install
|
13
12
|
|
@@ -33,7 +32,7 @@ Where:
|
|
33
32
|
- `store_name` The name of the store.
|
34
33
|
- `name` The name of the accessor to the store.
|
35
34
|
- `type` A symbol such as `:string` or `:integer`, or a type object to be used for the accessor.
|
36
|
-
- `options` (optional) A hash of cast type options such as `precision`, `limit`, `scale`.
|
35
|
+
- `options` (optional) A hash of cast type options such as `precision`, `limit`, `scale`, `default`.
|
37
36
|
|
38
37
|
Type casting occurs every time you write data through accessor or update store itself
|
39
38
|
and when object is loaded from database.
|
@@ -47,6 +46,8 @@ class MegaUser < User
|
|
47
46
|
store_attribute :settings, :ratio, :integer, limit: 1
|
48
47
|
store_attribute :settings, :login_at, :datetime
|
49
48
|
store_attribute :settings, :active, :boolean
|
49
|
+
store_attribute :settings, :color, :string, default: "red"
|
50
|
+
store_attribute :settings, :data, :datetime, default: -> { Time.now }
|
50
51
|
end
|
51
52
|
|
52
53
|
u = MegaUser.new(active: false, login_at: "2015-01-01 00:01", ratio: "63.4608")
|
@@ -55,6 +56,10 @@ u.login_at.is_a?(DateTime) # => true
|
|
55
56
|
u.login_at = DateTime.new(2015, 1, 1, 11, 0, 0)
|
56
57
|
u.ratio # => 63
|
57
58
|
u.active # => false
|
59
|
+
# Default value is set
|
60
|
+
u.color # => red
|
61
|
+
# A dynamic default can also be provided
|
62
|
+
u.data # => Current time
|
58
63
|
# And we also have a predicate method
|
59
64
|
u.active? # => false
|
60
65
|
u.reload
|
@@ -15,43 +15,57 @@ module ActiveRecord
|
|
15
15
|
|
16
16
|
def initialize(subtype)
|
17
17
|
@accessor_types = {}
|
18
|
+
@defaults = {}
|
18
19
|
@store_accessor = subtype.accessor
|
19
20
|
super(subtype)
|
20
21
|
end
|
21
22
|
|
22
|
-
|
23
|
+
UNDEFINED = Object.new
|
24
|
+
private_constant :UNDEFINED
|
25
|
+
|
26
|
+
def add_typed_key(key, type, default: UNDEFINED, **options)
|
23
27
|
type = ActiveRecord::Type.lookup(type, options) if type.is_a?(Symbol)
|
24
|
-
|
28
|
+
safe_key = key.to_s
|
29
|
+
@accessor_types[safe_key] = type
|
30
|
+
@defaults[safe_key] = default unless default == UNDEFINED
|
25
31
|
end
|
26
32
|
|
27
33
|
def deserialize(value)
|
28
34
|
hash = super
|
29
|
-
|
30
|
-
|
31
|
-
|
35
|
+
return hash unless hash
|
36
|
+
accessor_types.each do |key, type|
|
37
|
+
if hash.key?(key)
|
38
|
+
hash[key] = type.deserialize(hash[key])
|
39
|
+
elsif defaults.key?(key)
|
40
|
+
hash[key] = get_default(key)
|
32
41
|
end
|
33
42
|
end
|
34
43
|
hash
|
35
44
|
end
|
36
45
|
|
37
46
|
def serialize(value)
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
47
|
+
return super(value) unless value.is_a?(Hash)
|
48
|
+
typed_casted = {}
|
49
|
+
accessor_types.each do |str_key, type|
|
50
|
+
key = key_to_cast(value, str_key)
|
51
|
+
next unless key
|
52
|
+
if value.key?(key)
|
53
|
+
typed_casted[key] = type.serialize(value[key])
|
54
|
+
elsif defaults.key?(str_key)
|
55
|
+
typed_casted[key] = type.serialize(get_default(str_key))
|
43
56
|
end
|
44
|
-
super(value.merge(typed_casted))
|
45
|
-
else
|
46
|
-
super(value)
|
47
57
|
end
|
58
|
+
super(value.merge(typed_casted))
|
48
59
|
end
|
49
60
|
|
50
61
|
def cast(value)
|
51
62
|
hash = super
|
52
|
-
|
53
|
-
|
54
|
-
|
63
|
+
return hash unless hash
|
64
|
+
accessor_types.each do |key, type|
|
65
|
+
if hash.key?(key)
|
66
|
+
hash[key] = type.cast(hash[key])
|
67
|
+
elsif defaults.key?(key)
|
68
|
+
hash[key] = get_default(key)
|
55
69
|
end
|
56
70
|
end
|
57
71
|
hash
|
@@ -74,6 +88,7 @@ module ActiveRecord
|
|
74
88
|
def key_to_cast(val, key)
|
75
89
|
return key if val.key?(key)
|
76
90
|
return key.to_sym if val.key?(key.to_sym)
|
91
|
+
return key if defaults.key?(key)
|
77
92
|
end
|
78
93
|
|
79
94
|
def typed?(key)
|
@@ -84,7 +99,12 @@ module ActiveRecord
|
|
84
99
|
accessor_types.fetch(key.to_s)
|
85
100
|
end
|
86
101
|
|
87
|
-
|
102
|
+
def get_default(key)
|
103
|
+
value = defaults.fetch(key)
|
104
|
+
value.is_a?(Proc) ? value.call : value
|
105
|
+
end
|
106
|
+
|
107
|
+
attr_reader :accessor_types, :defaults, :store_accessor
|
88
108
|
end
|
89
109
|
end
|
90
110
|
end
|
@@ -21,6 +21,9 @@ describe StoreAttribute do
|
|
21
21
|
@connection.drop_table "users", if_exists: true
|
22
22
|
end
|
23
23
|
|
24
|
+
let(:date) { Date.new(2019, 7, 17) }
|
25
|
+
let(:default_date) { User::DEFAULT_DATE }
|
26
|
+
let(:dynamic_date) { User::TODAY_DATE }
|
24
27
|
let(:time) { DateTime.new(2015, 2, 14, 17, 0, 0) }
|
25
28
|
let(:time_str) { "2015-02-14 17:00" }
|
26
29
|
let(:time_str_utc) { "2015-02-14 17:00:00 UTC" }
|
@@ -120,7 +123,12 @@ describe StoreAttribute do
|
|
120
123
|
|
121
124
|
it "re-typecast old data" do
|
122
125
|
jamie = User.create!
|
123
|
-
User.update_all(
|
126
|
+
User.update_all(
|
127
|
+
"jparams = '{"\
|
128
|
+
'"active":"1",'\
|
129
|
+
'"salary":"12.02"'\
|
130
|
+
"}'::jsonb"
|
131
|
+
)
|
124
132
|
|
125
133
|
jamie = User.find(jamie.id)
|
126
134
|
expect(jamie).to be_active
|
@@ -170,4 +178,39 @@ describe StoreAttribute do
|
|
170
178
|
expect(jamie.inner_json).to eq("x" => 1)
|
171
179
|
end
|
172
180
|
end
|
181
|
+
|
182
|
+
context "default option" do
|
183
|
+
it "should init the field after an object is created" do
|
184
|
+
jamie = User.new
|
185
|
+
expect(jamie.static_date).to eq(default_date)
|
186
|
+
end
|
187
|
+
|
188
|
+
it "should not affect explicit initialization" do
|
189
|
+
jamie = User.new(static_date: date)
|
190
|
+
expect(jamie.static_date).to eq(date)
|
191
|
+
end
|
192
|
+
|
193
|
+
it "should not affect explicit nil initialization" do
|
194
|
+
jamie = User.new(static_date: nil)
|
195
|
+
expect(jamie.static_date).to be_nil
|
196
|
+
end
|
197
|
+
|
198
|
+
it "should handle a static value" do
|
199
|
+
jamie = User.create!
|
200
|
+
jamie = User.find(jamie.id)
|
201
|
+
expect(jamie.static_date).to eq(default_date)
|
202
|
+
end
|
203
|
+
|
204
|
+
it "should handle a lambda" do
|
205
|
+
jamie = User.create!
|
206
|
+
jamie = User.find(jamie.id)
|
207
|
+
expect(jamie.dynamic_date).to eq(dynamic_date)
|
208
|
+
end
|
209
|
+
|
210
|
+
it "should handle nil" do
|
211
|
+
jamie = User.create!
|
212
|
+
jamie = User.find(jamie.id)
|
213
|
+
expect(jamie.empty_date).to be_nil
|
214
|
+
end
|
215
|
+
end
|
173
216
|
end
|
@@ -20,6 +20,37 @@ describe ActiveRecord::Type::TypedStore do
|
|
20
20
|
date = ::Date.new(2016, 6, 22)
|
21
21
|
expect(subject.cast(date: "2016-06-22")).to eq("date" => date)
|
22
22
|
end
|
23
|
+
|
24
|
+
it "with default" do
|
25
|
+
date = ::Date.new(2016, 6, 22)
|
26
|
+
subject.add_typed_key("date", :date, default: date)
|
27
|
+
|
28
|
+
expect(subject.cast({})).to eq("date" => date)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "with dynamic default" do
|
32
|
+
date = ::Date.new(2016, 6, 22)
|
33
|
+
subject.add_typed_key("date", :date, default: -> { date })
|
34
|
+
|
35
|
+
expect(subject.cast({})).to eq("date" => date)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "with default and explicit nil" do
|
39
|
+
date = ::Date.new(2016, 6, 22)
|
40
|
+
subject.add_typed_key("date", :date, default: date)
|
41
|
+
nil_date = {"date" => nil}
|
42
|
+
|
43
|
+
expect(subject.cast(nil_date)).to eq(nil_date)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "with default and existing value" do
|
47
|
+
default = ::Date.new(2016, 6, 22)
|
48
|
+
expected = ::Date.new(2019, 7, 17)
|
49
|
+
subject.add_typed_key("date", :date, default: default)
|
50
|
+
date = {"date" => expected}
|
51
|
+
|
52
|
+
expect(subject.cast("date" => expected.to_s)).to eq(date)
|
53
|
+
end
|
23
54
|
end
|
24
55
|
|
25
56
|
describe "#deserialize" do
|
@@ -34,6 +65,37 @@ describe ActiveRecord::Type::TypedStore do
|
|
34
65
|
date = ::Date.new(2016, 6, 22)
|
35
66
|
expect(subject.deserialize('{"date":"2016-06-22"}')).to eq("date" => date)
|
36
67
|
end
|
68
|
+
|
69
|
+
it "with default" do
|
70
|
+
date = ::Date.new(2016, 6, 22)
|
71
|
+
subject.add_typed_key("date", :date, default: date)
|
72
|
+
|
73
|
+
expect(subject.deserialize("{}")).to eq("date" => date)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "with dynamic default" do
|
77
|
+
date = ::Date.new(2016, 6, 22)
|
78
|
+
subject.add_typed_key("date", :date, default: -> { date })
|
79
|
+
|
80
|
+
expect(subject.deserialize("{}")).to eq("date" => date)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "with default and explicit nil" do
|
84
|
+
date = ::Date.new(2016, 6, 22)
|
85
|
+
subject.add_typed_key("date", :date, default: date)
|
86
|
+
s11n = {"date" => nil}.to_json
|
87
|
+
|
88
|
+
expect(subject.deserialize(s11n)).to eq("date" => nil)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "with default and existing value" do
|
92
|
+
default = ::Date.new(2016, 6, 22)
|
93
|
+
expected = ::Date.new(2019, 7, 17)
|
94
|
+
subject.add_typed_key("date", :date, default: default)
|
95
|
+
s11n = {"date" => expected}.to_json
|
96
|
+
|
97
|
+
expect(subject.deserialize(s11n)).to eq("date" => expected)
|
98
|
+
end
|
37
99
|
end
|
38
100
|
|
39
101
|
describe "#serialize" do
|
@@ -54,6 +116,25 @@ describe ActiveRecord::Type::TypedStore do
|
|
54
116
|
|
55
117
|
expect { subject.serialize(val: 1024) }.to raise_error(RangeError)
|
56
118
|
end
|
119
|
+
|
120
|
+
it "with type key and a default" do
|
121
|
+
date = ::Date.new(2016, 6, 22)
|
122
|
+
subject.add_typed_key("date", :date, default: date)
|
123
|
+
expect(subject.serialize({})).to eq({date: date}.to_json)
|
124
|
+
end
|
125
|
+
|
126
|
+
it "with type key and a dynamic default" do
|
127
|
+
date = ::Date.today
|
128
|
+
default = -> { date }
|
129
|
+
subject.add_typed_key("date", :date, default: default)
|
130
|
+
expect(subject.serialize({})).to eq({date: date}.to_json)
|
131
|
+
end
|
132
|
+
|
133
|
+
it "with a symbolic type key" do
|
134
|
+
date = ::Date.new(2016, 6, 22)
|
135
|
+
subject.add_typed_key(:date, :date, default: date)
|
136
|
+
expect(subject.serialize({})).to eq({date: date}.to_json)
|
137
|
+
end
|
57
138
|
end
|
58
139
|
|
59
140
|
describe ".create_from_type" do
|
@@ -66,6 +147,20 @@ describe ActiveRecord::Type::TypedStore do
|
|
66
147
|
expect(type.cast(date: "2016-06-22", val: "1.2")).to eq("date" => date, "val" => "1.2")
|
67
148
|
expect(new_type.cast(date: "2016-06-22", val: "1.2")).to eq("date" => date, "val" => 1)
|
68
149
|
end
|
150
|
+
|
151
|
+
it "accepts default", :aggregate_failures do
|
152
|
+
default = 1
|
153
|
+
type = described_class.create_from_type(json_type, "val", :date, default: default)
|
154
|
+
|
155
|
+
expect(type.cast({})).to eq("val" => default)
|
156
|
+
end
|
157
|
+
|
158
|
+
it "accepts dynamic default", :aggregate_failures do
|
159
|
+
default = 1
|
160
|
+
type = described_class.create_from_type(json_type, "val", :date, default: -> { default })
|
161
|
+
|
162
|
+
expect(type.cast({})).to eq("val" => default)
|
163
|
+
end
|
69
164
|
end
|
70
165
|
end
|
71
166
|
|
@@ -105,5 +200,41 @@ describe ActiveRecord::Type::TypedStore do
|
|
105
200
|
expect(subject.serialize(date: date)).to eq "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\ndate: 2016-06-22\n"
|
106
201
|
expect(subject.serialize("date" => date)).to eq "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\ndate: 2016-06-22\n"
|
107
202
|
end
|
203
|
+
|
204
|
+
it "uses default" do
|
205
|
+
default = ::Date.new(2019, 7, 17)
|
206
|
+
subject.add_typed_key("date", :date, default: default)
|
207
|
+
|
208
|
+
expect(subject.deserialize("---\n")).to eq("date" => default)
|
209
|
+
end
|
210
|
+
|
211
|
+
it "uses dynamic default" do
|
212
|
+
default = -> { ::Date.new(2019, 7, 17) }
|
213
|
+
subject.add_typed_key("date", :date, default: default)
|
214
|
+
|
215
|
+
expect(subject.deserialize("---\n")).to eq("date" => default.call)
|
216
|
+
end
|
217
|
+
|
218
|
+
it "keeps explicit nil" do
|
219
|
+
default = ::Date.new(2019, 7, 17)
|
220
|
+
subject.add_typed_key("date", :date, default: default)
|
221
|
+
|
222
|
+
expect(subject.deserialize("---\ndate: null\n")).to eq("date" => nil)
|
223
|
+
end
|
224
|
+
|
225
|
+
it "keeps existing value" do
|
226
|
+
default = ::Date.new(2019, 7, 17)
|
227
|
+
date = ::Date.new(2016, 6, 22)
|
228
|
+
subject.add_typed_key("date", :date, default: default)
|
229
|
+
|
230
|
+
expect(subject.deserialize("---\ndate: #{date}\n")).to eq("date" => date)
|
231
|
+
end
|
232
|
+
|
233
|
+
it "with a default" do
|
234
|
+
default = ::Date.new(2019, 7, 17)
|
235
|
+
subject.add_typed_key("date", :date, default: default)
|
236
|
+
expected = "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\ndate: 2019-07-17\n"
|
237
|
+
expect(subject.serialize({})).to eq(expected)
|
238
|
+
end
|
108
239
|
end
|
109
240
|
end
|
data/spec/support/user.rb
CHANGED
@@ -5,9 +5,14 @@ class RawUser < ActiveRecord::Base
|
|
5
5
|
end
|
6
6
|
|
7
7
|
class User < ActiveRecord::Base
|
8
|
+
DEFAULT_DATE = ::Date.new(2019, 7, 17)
|
9
|
+
TODAY_DATE = ::Date.today
|
10
|
+
|
8
11
|
store_accessor :jparams, :version, active: :boolean, salary: :integer
|
9
12
|
store_attribute :jparams, :birthday, :date
|
10
|
-
|
13
|
+
store_attribute :jparams, :static_date, :date, default: DEFAULT_DATE
|
14
|
+
store_attribute :jparams, :dynamic_date, :date, default: -> { TODAY_DATE }
|
15
|
+
store_attribute :jparams, :empty_date, :date, default: nil
|
11
16
|
store_attribute :jparams, :inner_json, :json
|
12
17
|
|
13
18
|
store :custom, accessors: [price: :money_type]
|
data/store_attribute.gemspec
CHANGED
@@ -21,6 +21,14 @@ Gem::Specification.new do |s|
|
|
21
21
|
|
22
22
|
s.required_ruby_version = ">= 2.4.0"
|
23
23
|
|
24
|
+
s.metadata = {
|
25
|
+
"bug_tracker_uri" => "http://github.com/palkan/store_attribute/issues",
|
26
|
+
"changelog_uri" => "https://github.com/palkan/store_attribute/blob/master/CHANGELOG.md",
|
27
|
+
"documentation_uri" => "http://github.com/palkan/store_attribute",
|
28
|
+
"homepage_uri" => "http://github.com/palkan/store_attribute",
|
29
|
+
"source_code_uri" => "http://github.com/palkan/store_attribute"
|
30
|
+
}
|
31
|
+
|
24
32
|
s.add_runtime_dependency "activerecord", ">= 5.0"
|
25
33
|
|
26
34
|
s.add_development_dependency "pg", ">= 0.18"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: store_attribute
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- palkan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-07-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -147,6 +147,7 @@ files:
|
|
147
147
|
- ".rspec"
|
148
148
|
- ".rubocop.yml"
|
149
149
|
- ".travis.yml"
|
150
|
+
- CHANGELOG.md
|
150
151
|
- Gemfile
|
151
152
|
- MIT-LICENSE
|
152
153
|
- README.md
|
@@ -172,7 +173,12 @@ files:
|
|
172
173
|
homepage: http://github.com/palkan/store_attribute
|
173
174
|
licenses:
|
174
175
|
- MIT
|
175
|
-
metadata:
|
176
|
+
metadata:
|
177
|
+
bug_tracker_uri: http://github.com/palkan/store_attribute/issues
|
178
|
+
changelog_uri: https://github.com/palkan/store_attribute/blob/master/CHANGELOG.md
|
179
|
+
documentation_uri: http://github.com/palkan/store_attribute
|
180
|
+
homepage_uri: http://github.com/palkan/store_attribute
|
181
|
+
source_code_uri: http://github.com/palkan/store_attribute
|
176
182
|
post_install_message:
|
177
183
|
rdoc_options: []
|
178
184
|
require_paths:
|
@@ -188,7 +194,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
188
194
|
- !ruby/object:Gem::Version
|
189
195
|
version: '0'
|
190
196
|
requirements: []
|
191
|
-
rubygems_version: 3.0.
|
197
|
+
rubygems_version: 3.0.3
|
192
198
|
signing_key:
|
193
199
|
specification_version: 4
|
194
200
|
summary: ActiveRecord extension which adds typecasting to store accessors
|