store_attribute 0.5.3 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26bfe00fc2e06215356a2e050700f0d257e56401249c97487e26f58133f4ea0e
4
- data.tar.gz: 5e9ba6def76cf0f0aa77130db602929da9725f1483508e244557e90e8bd22892
3
+ metadata.gz: 23a5221f6ede53f448653e54b3aa7432134c56e06369162a5370470e954fd1a1
4
+ data.tar.gz: 8df0761793ef5c39cda9c97039ca501fa8f9393fcb3ecc8abf85367414e3533c
5
5
  SHA512:
6
- metadata.gz: 3de11ae80f51e606826b3dbbdd92afa4a7443ec24ef595c616908adb23f2588908052f5db52d963164843aa0868c30747b54921e9fdac2b7deaede4b770413b3
7
- data.tar.gz: 6032541f1dd51712f9755b9ca8ccfa25b145c670d319a3dde909917fb0b7447069600a3fdd7c8fce40e131733fa6495e7be420f7a77aa5f60a88c36c28301f3a
6
+ metadata.gz: 2b4cedb5566a137159e863a59dc38797dcf7ddc590f779a957a6b6f6ac73e2c8f05f82876649304143f5160eefcbfc001773e9219d6d1d044fe9f483bad9c426
7
+ data.tar.gz: f95f478824a19e55243ca1b1cda44c284db8429b9afcad333fb7933ebca2c2ef54f0fbe98095523f1564a15e31f3a236e1b5546a249023c0c196635d8ec175c7
@@ -0,0 +1,39 @@
1
+ # Change log
2
+
3
+ ## master
4
+
5
+ ## 0.8.1 (2020-12-03)
6
+
7
+ - Fix adding dirty tracking methods for `store_attribute`. ([@palkan][])
8
+
9
+ ## 0.8.0
10
+
11
+ - Add Rails 6.1 compatibility. ([@palkan][])
12
+
13
+ - Add support for `prefix` and `suffix` options. ([@palkan][])
14
+
15
+ ## 0.7.1
16
+
17
+ - Fixed bug with `store` called without accessors. ([@ioki-klaus][])
18
+
19
+ See [#10](https://github.com/palkan/store_attribute/pull/10).
20
+
21
+ ## 0.7.0 (2020-03-23)
22
+
23
+ - Added dirty tracking methods. ([@glaszig][])
24
+
25
+ [PR #8](https://github.com/palkan/store_attribute/pull/8).
26
+
27
+ ## 0.6.0 (2019-07-24)
28
+
29
+ - Added default values support. ([@dreikanter][], [@SumLare][])
30
+
31
+ See [PR #7](https://github.com/palkan/store_attribute/pull/7).
32
+
33
+ - Start keeping changelog. ([@palkan][])
34
+
35
+ [@palkan]: https://github.com/palkan
36
+ [@dreikanter]: https://github.com/dreikanter
37
+ [@SumLare]: https://github.com/SumLare
38
+ [@glaszig]: https://github.com/glaszig
39
+ [@ioki-klaus]: https://github.com/ioki-klaus
@@ -1,4 +1,4 @@
1
- Copyright 2016 palkan
1
+ Copyright 2016-2020 palkan
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,3 +1,4 @@
1
+ [![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](https://cultofmartians.com/tasks/store-attribute-defaults.html#task)
1
2
  [![Gem Version](https://badge.fury.io/rb/store_attribute.svg)](https://rubygems.org/gems/store_attribute) [![Build Status](https://travis-ci.org/palkan/store_attribute.svg?branch=master)](https://travis-ci.org/palkan/store_attribute)
2
3
 
3
4
  ## Store Attribute
@@ -6,8 +7,7 @@ ActiveRecord extension which adds typecasting to store accessors.
6
7
 
7
8
  Compatible with Rails 4.2 and Rails 5+.
8
9
 
9
- <a href="https://evilmartians.com/">
10
- <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
10
+ Extracted from not merged PR to Rails: [rails/rails#18942](https://github.com/rails/rails/pull/18942).
11
11
 
12
12
  ### Install
13
13
 
@@ -33,7 +33,7 @@ Where:
33
33
  - `store_name` The name of the store.
34
34
  - `name` The name of the accessor to the store.
35
35
  - `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`.
36
+ - `options` (optional) A hash of cast type options such as `precision`, `limit`, `scale`, `default`.
37
37
 
38
38
  Type casting occurs every time you write data through accessor or update store itself
39
39
  and when object is loaded from database.
@@ -47,6 +47,8 @@ class MegaUser < User
47
47
  store_attribute :settings, :ratio, :integer, limit: 1
48
48
  store_attribute :settings, :login_at, :datetime
49
49
  store_attribute :settings, :active, :boolean
50
+ store_attribute :settings, :color, :string, default: "red"
51
+ store_attribute :settings, :data, :datetime, default: -> { Time.now }
50
52
  end
51
53
 
52
54
  u = MegaUser.new(active: false, login_at: "2015-01-01 00:01", ratio: "63.4608")
@@ -55,6 +57,10 @@ u.login_at.is_a?(DateTime) # => true
55
57
  u.login_at = DateTime.new(2015, 1, 1, 11, 0, 0)
56
58
  u.ratio # => 63
57
59
  u.active # => false
60
+ # Default value is set
61
+ u.color # => red
62
+ # A dynamic default can also be provided
63
+ u.data # => Current time
58
64
  # And we also have a predicate method
59
65
  u.active? # => false
60
66
  u.reload
@@ -25,8 +25,15 @@ module ActiveRecord
25
25
  # end
26
26
  def store(store_name, options = {})
27
27
  accessors = options.delete(:accessors)
28
+ typed_accessors =
29
+ if accessors && accessors.last.is_a?(Hash)
30
+ accessors.pop
31
+ else
32
+ {}
33
+ end
34
+
28
35
  _orig_store(store_name, options)
29
- store_accessor(store_name, *accessors) if accessors
36
+ store_accessor(store_name, *accessors, **typed_accessors) if accessors
30
37
  end
31
38
 
32
39
  # Adds additional accessors to an existing store on this model.
@@ -37,21 +44,29 @@ module ActiveRecord
37
44
  #
38
45
  # +typed_keys+ The key-to-type hash of the accesors with type to the store.
39
46
  #
47
+ # +prefix+ Accessor method name prefix
48
+ #
49
+ # +suffix+ Accessor method name suffix
50
+ #
40
51
  # Examples:
41
52
  #
42
53
  # class SuperUser < User
43
54
  # store_accessor :settings, :privileges, login_at: :datetime
44
55
  # end
45
- def store_accessor(store_name, *keys, **typed_keys)
56
+ def store_accessor(store_name, *keys, prefix: nil, suffix: nil, **typed_keys)
46
57
  keys = keys.flatten
47
58
  typed_keys = typed_keys.except(keys)
48
59
 
49
- _define_accessors_methods(store_name, *keys)
60
+ accessor_prefix, accessor_suffix = _normalize_prefix_suffix(store_name, prefix, suffix)
61
+
62
+ _define_accessors_methods(store_name, *keys, prefix: accessor_prefix, suffix: accessor_suffix)
63
+
64
+ _define_dirty_tracking_methods(store_name, keys + typed_keys.keys, prefix: accessor_prefix, suffix: accessor_suffix)
50
65
 
51
66
  _prepare_local_stored_attributes(store_name, *keys)
52
67
 
53
68
  typed_keys.each do |key, type|
54
- store_attribute(store_name, key, type)
69
+ store_attribute(store_name, key, type, prefix: prefix, suffix: suffix)
55
70
  end
56
71
  end
57
72
 
@@ -68,6 +83,10 @@ module ActiveRecord
68
83
  # +type+ A symbol such as +:string+ or +:integer+, or a type object
69
84
  # to be used for the accessor.
70
85
  #
86
+ # +prefix+ Accessor method name prefix
87
+ #
88
+ # +suffix+ Accessor method name suffix
89
+ #
71
90
  # +options+ A hash of cast type options such as +precision+, +limit+, +scale+.
72
91
  #
73
92
  # Examples:
@@ -75,13 +94,16 @@ module ActiveRecord
75
94
  # class MegaUser < User
76
95
  # store_attribute :settings, :ratio, :integer, limit: 1
77
96
  # store_attribute :settings, :login_at, :datetime
97
+ #
98
+ # store_attribute :extra, :version, :integer, prefix: :meta
78
99
  # end
79
100
  #
80
- # u = MegaUser.new(active: false, login_at: '2015-01-01 00:01', ratio: "63.4608")
101
+ # u = MegaUser.new(active: false, login_at: '2015-01-01 00:01', ratio: "63.4608", meta_version: "1")
81
102
  #
82
103
  # u.login_at.is_a?(DateTime) # => true
83
104
  # u.login_at = DateTime.new(2015,1,1,11,0,0)
84
105
  # u.ratio # => 63
106
+ # u.meta_version #=> 1
85
107
  # u.reload
86
108
  #
87
109
  # # After loading record from db store contains casted data
@@ -98,15 +120,28 @@ module ActiveRecord
98
120
  # u.settings['ratio'] # => 3
99
121
  #
100
122
  # For more examples on using types, see documentation for ActiveRecord::Attributes.
101
- def store_attribute(store_name, name, type, **options)
102
- _define_accessors_methods(store_name, name)
123
+ def store_attribute(store_name, name, type, prefix: nil, suffix: nil, **options)
124
+ prefix, suffix = _normalize_prefix_suffix(store_name, prefix, suffix)
125
+
126
+ _define_accessors_methods(store_name, name, prefix: prefix, suffix: suffix)
103
127
 
104
- _define_predicate_method(name) if type == :boolean
128
+ _define_predicate_method(name, prefix: prefix, suffix: suffix) if type == :boolean
105
129
 
106
- decorate_attribute_type(store_name, "typed_accessor_for_#{name}") do |subtype|
107
- Type::TypedStore.create_from_type(subtype, name, type, **options)
130
+ # Rails >6.0
131
+ if method(:decorate_attribute_type).parameters.count { |type, _| type == :req } == 1
132
+ attr_name = store_name.to_s
133
+ was_type = attributes_to_define_after_schema_loads[attr_name]&.first
134
+ attribute(attr_name) do |subtype|
135
+ Type::TypedStore.create_from_type(_lookup_cast_type(attr_name, was_type, {}), name, type, **options)
136
+ end
137
+ else
138
+ decorate_attribute_type(store_name, "typed_accessor_for_#{name}") do |subtype|
139
+ Type::TypedStore.create_from_type(subtype, name, type, **options)
140
+ end
108
141
  end
109
142
 
143
+ _define_dirty_tracking_methods(store_name, [name], prefix: prefix, suffix: suffix)
144
+
110
145
  _prepare_local_stored_attributes(store_name, name)
111
146
  end
112
147
 
@@ -118,27 +153,96 @@ module ActiveRecord
118
153
  self.local_stored_attributes[store_name] |= keys
119
154
  end
120
155
 
121
- def _define_accessors_methods(store_name, *keys) # :nodoc:
156
+ def _define_accessors_methods(store_name, *keys, prefix: nil, suffix: nil) # :nodoc:
122
157
  _store_accessors_module.module_eval do
123
158
  keys.each do |key|
124
- define_method("#{key}=") do |value|
159
+ accessor_key = "#{prefix}#{key}#{suffix}"
160
+
161
+ define_method("#{accessor_key}=") do |value|
125
162
  write_store_attribute(store_name, key, value)
126
163
  end
127
164
 
128
- define_method(key) do
165
+ define_method(accessor_key) do
129
166
  read_store_attribute(store_name, key)
130
167
  end
131
168
  end
132
169
  end
133
170
  end
134
171
 
135
- def _define_predicate_method(name)
172
+ def _define_predicate_method(name, prefix: nil, suffix: nil)
136
173
  _store_accessors_module.module_eval do
174
+ name = "#{prefix}#{name}#{suffix}"
175
+
137
176
  define_method("#{name}?") do
138
177
  send(name) == true
139
178
  end
140
179
  end
141
180
  end
181
+
182
+ def _define_dirty_tracking_methods(store_attribute, keys, prefix: nil, suffix: nil)
183
+ _store_accessors_module.module_eval do
184
+ keys.flatten.each do |key|
185
+ key = key.to_s
186
+ accessor_key = "#{prefix}#{key}#{suffix}"
187
+
188
+ define_method("#{accessor_key}_changed?") do
189
+ return false unless attribute_changed?(store_attribute)
190
+ prev_store, new_store = changes[store_attribute]
191
+ prev_store&.dig(key) != new_store&.dig(key)
192
+ end
193
+
194
+ define_method("#{accessor_key}_change") do
195
+ return unless attribute_changed?(store_attribute)
196
+ prev_store, new_store = changes[store_attribute]
197
+ [prev_store&.dig(key), new_store&.dig(key)]
198
+ end
199
+
200
+ define_method("#{accessor_key}_was") do
201
+ return unless attribute_changed?(store_attribute)
202
+ prev_store, _new_store = changes[store_attribute]
203
+ prev_store&.dig(key)
204
+ end
205
+
206
+ define_method("saved_change_to_#{accessor_key}?") do
207
+ return false unless saved_change_to_attribute?(store_attribute)
208
+ prev_store, new_store = saved_change_to_attribute(store_attribute)
209
+ prev_store&.dig(key) != new_store&.dig(key)
210
+ end
211
+
212
+ define_method("saved_change_to_#{accessor_key}") do
213
+ return unless saved_change_to_attribute?(store_attribute)
214
+ prev_store, new_store = saved_change_to_attribute(store_attribute)
215
+ [prev_store&.dig(key), new_store&.dig(key)]
216
+ end
217
+
218
+ define_method("#{accessor_key}_before_last_save") do
219
+ return unless saved_change_to_attribute?(store_attribute)
220
+ prev_store, _new_store = saved_change_to_attribute(store_attribute)
221
+ prev_store&.dig(key)
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ def _normalize_prefix_suffix(store_name, prefix, suffix)
228
+ prefix =
229
+ case prefix
230
+ when String, Symbol
231
+ "#{prefix}_"
232
+ when TrueClass
233
+ "#{store_name}_"
234
+ end
235
+
236
+ suffix =
237
+ case suffix
238
+ when String, Symbol
239
+ "_#{suffix}"
240
+ when TrueClass
241
+ "_#{store_name}"
242
+ end
243
+
244
+ [prefix, suffix]
245
+ end
142
246
  end
143
247
  end
144
248
  end
@@ -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
- def add_typed_key(key, type, **options)
23
- type = ActiveRecord::Type.lookup(type, options) if type.is_a?(Symbol)
24
- @accessor_types[key.to_s] = type
23
+ UNDEFINED = Object.new
24
+ private_constant :UNDEFINED
25
+
26
+ def add_typed_key(key, type, default: UNDEFINED, **options)
27
+ type = ActiveRecord::Type.lookup(type, **options) if type.is_a?(Symbol)
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
- if hash
30
- accessor_types.each do |key, type|
31
- hash[key] = type.deserialize(hash[key]) if hash.key?(key)
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
- if value.is_a?(Hash)
39
- typed_casted = {}
40
- accessor_types.each do |key, type|
41
- k = key_to_cast(value, key)
42
- typed_casted[k] = type.serialize(value[k]) unless k.nil?
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
- if hash
53
- accessor_types.each do |key, type|
54
- hash[key] = type.cast(hash[key]) if hash.key?(key)
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
- attr_reader :accessor_types, :store_accessor
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StoreAttribute # :nodoc:
4
- VERSION = "0.5.3"
4
+ VERSION = "0.8.1"
5
5
  end
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.5.3
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-10 00:00:00.000000000 Z
11
+ date: 2020-12-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -44,84 +44,42 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '10.1'
47
+ version: '13.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '10.1'
55
- - !ruby/object:Gem::Dependency
56
- name: simplecov
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: 0.3.8
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: 0.3.8
69
- - !ruby/object:Gem::Dependency
70
- name: pry-byebug
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: rubocop
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: 0.65.0
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: 0.65.0
54
+ version: '13.0'
97
55
  - !ruby/object:Gem::Dependency
98
56
  name: rubocop-md
99
57
  requirement: !ruby/object:Gem::Requirement
100
58
  requirements:
101
59
  - - "~>"
102
60
  - !ruby/object:Gem::Version
103
- version: '0.2'
61
+ version: '0.3'
104
62
  type: :development
105
63
  prerelease: false
106
64
  version_requirements: !ruby/object:Gem::Requirement
107
65
  requirements:
108
66
  - - "~>"
109
67
  - !ruby/object:Gem::Version
110
- version: '0.2'
68
+ version: '0.3'
111
69
  - !ruby/object:Gem::Dependency
112
70
  name: standard
113
71
  requirement: !ruby/object:Gem::Requirement
114
72
  requirements:
115
73
  - - "~>"
116
74
  - !ruby/object:Gem::Version
117
- version: 0.0.36
75
+ version: 0.2.0
118
76
  type: :development
119
77
  prerelease: false
120
78
  version_requirements: !ruby/object:Gem::Requirement
121
79
  requirements:
122
80
  - - "~>"
123
81
  - !ruby/object:Gem::Version
124
- version: 0.0.36
82
+ version: 0.2.0
125
83
  - !ruby/object:Gem::Dependency
126
84
  name: rspec
127
85
  requirement: !ruby/object:Gem::Requirement
@@ -143,36 +101,23 @@ executables: []
143
101
  extensions: []
144
102
  extra_rdoc_files: []
145
103
  files:
146
- - ".gitignore"
147
- - ".rspec"
148
- - ".rubocop.yml"
149
- - ".travis.yml"
150
- - Gemfile
151
- - MIT-LICENSE
104
+ - CHANGELOG.md
105
+ - LICENSE.txt
152
106
  - README.md
153
- - Rakefile
154
- - bench/bench.rb
155
- - bench/setup.rb
156
- - bin/console
157
- - bin/setup
158
- - gemfiles/rails5.gemfile
159
- - gemfiles/rails6.gemfile
160
- - gemfiles/railsmaster.gemfile
161
107
  - lib/store_attribute.rb
162
108
  - lib/store_attribute/active_record.rb
163
109
  - lib/store_attribute/active_record/store.rb
164
110
  - lib/store_attribute/active_record/type/typed_store.rb
165
111
  - lib/store_attribute/version.rb
166
- - spec/cases/store_attribute_spec.rb
167
- - spec/spec_helper.rb
168
- - spec/store_attribute/typed_store_spec.rb
169
- - spec/support/money_type.rb
170
- - spec/support/user.rb
171
- - store_attribute.gemspec
172
112
  homepage: http://github.com/palkan/store_attribute
173
113
  licenses:
174
114
  - MIT
175
- metadata: {}
115
+ metadata:
116
+ bug_tracker_uri: http://github.com/palkan/store_attribute/issues
117
+ changelog_uri: https://github.com/palkan/store_attribute/blob/master/CHANGELOG.md
118
+ documentation_uri: http://github.com/palkan/store_attribute
119
+ homepage_uri: http://github.com/palkan/store_attribute
120
+ source_code_uri: http://github.com/palkan/store_attribute
176
121
  post_install_message:
177
122
  rdoc_options: []
178
123
  require_paths:
@@ -188,7 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
188
133
  - !ruby/object:Gem::Version
189
134
  version: '0'
190
135
  requirements: []
191
- rubygems_version: 3.0.2
136
+ rubygems_version: 3.0.6
192
137
  signing_key:
193
138
  specification_version: 4
194
139
  summary: ActiveRecord extension which adds typecasting to store accessors
data/.gitignore DELETED
@@ -1,37 +0,0 @@
1
- # Numerous always-ignore extensions
2
- *.diff
3
- *.err
4
- *.orig
5
- *.log
6
- *.rej
7
- *.swo
8
- *.swp
9
- *.vi
10
- *~
11
- *.sass-cache
12
- *.iml
13
- .idea/
14
-
15
- # Sublime
16
- *.sublime-project
17
- *.sublime-workspace
18
-
19
- # OS or Editor folders
20
- .DS_Store
21
- .cache
22
- .project
23
- .settings
24
- .tmproj
25
- Thumbs.db
26
- coverage/
27
-
28
- .bundle/
29
- *.log
30
- *.gem
31
- pkg/
32
- spec/dummy/log/*.log
33
- spec/dummy/tmp/
34
- spec/dummy/.sass-cache
35
- Gemfile.local
36
- Gemfile.lock
37
- tmp/
data/.rspec DELETED
@@ -1 +0,0 @@
1
- --color
@@ -1,54 +0,0 @@
1
- require:
2
- - standard/cop/semantic_blocks
3
- - rubocop-md
4
-
5
- inherit_gem:
6
- standard: config/base.yml
7
-
8
- AllCops:
9
- Exclude:
10
- - 'bin/*'
11
- - 'tmp/**/*'
12
- - 'Gemfile'
13
- - 'vendor/**/*'
14
- - 'gemfiles/**/*'
15
- - 'bench/**/*'
16
- DisplayCopNames: true
17
- TargetRubyVersion: 2.4
18
-
19
- Standard/SemanticBlocks:
20
- Enabled: false
21
-
22
- Style/FrozenStringLiteralComment:
23
- Enabled: true
24
-
25
- Style/TrailingCommaInArrayLiteral:
26
- EnforcedStyleForMultiline: no_comma
27
-
28
- Style/TrailingCommaInHashLiteral:
29
- EnforcedStyleForMultiline: no_comma
30
-
31
- Layout/AlignParameters:
32
- EnforcedStyle: with_first_parameter
33
-
34
- Lint/Void:
35
- Exclude:
36
- - '**/*.md'
37
-
38
- # See https://github.com/rubocop-hq/rubocop/issues/4222
39
- Lint/AmbiguousBlockAssociation:
40
- Exclude:
41
- - 'spec/**/*'
42
- - '**/*.md'
43
-
44
- Lint/DuplicateMethods:
45
- Exclude:
46
- - '**/*.md'
47
-
48
- Naming/FileName:
49
- Exclude:
50
- - '**/*.md'
51
-
52
- Layout/InitialIndentation:
53
- Exclude:
54
- - 'CHANGELOG.md'
@@ -1,21 +0,0 @@
1
- language: ruby
2
- cache: bundler
3
-
4
- addons:
5
- postgresql: "9.4"
6
-
7
- before_script:
8
- - createdb store_attribute_test
9
- - psql -U postgres -d store_attribute_test -c 'CREATE EXTENSION IF NOT EXISTS hstore;'
10
-
11
- matrix:
12
- include:
13
- - rvm: 2.4.1
14
- gemfile: gemfiles/rails5.gemfile
15
- - rvm: 2.6.2
16
- gemfile: gemfiles/rails6.gemfile
17
- - rvm: ruby-head
18
- gemfile: gemfiles/railsmaster.gemfile
19
- allow_failures:
20
- - rvm: ruby-head
21
- gemfile: gemfiles/railsmaster.gemfile
data/Gemfile DELETED
@@ -1,11 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- gemspec
4
-
5
- local_gemfile = 'Gemfile.local'
6
-
7
- if File.exist?(local_gemfile)
8
- eval(File.read(local_gemfile)) # rubocop:disable Lint/Eval
9
- else
10
- gem 'activerecord', '5.1.0'
11
- end
data/Rakefile DELETED
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
- require "rubocop/rake_task"
6
-
7
- RSpec::Core::RakeTask.new(:spec)
8
- RuboCop::RakeTask.new
9
-
10
- task default: [:rubocop, :spec]
@@ -1,38 +0,0 @@
1
- require 'benchmark/ips'
2
- require './setup'
3
-
4
- Benchmark.ips do |x|
5
- x.report('SA initialize') do
6
- User.new(public: '1', published_at: '2016-01-01', age: '23')
7
- end
8
-
9
- x.report('AR-T initialize') do
10
- Looser.new(public: '1', published_at: '2016-01-01', age: '23')
11
- end
12
- end
13
-
14
- Benchmark.ips do |x|
15
- x.report('SA accessors') do
16
- u = User.new
17
- u.public = '1'
18
- u.published_at = '2016-01-01'
19
- u.age = '23'
20
- end
21
-
22
- x.report('AR-T accessors') do
23
- u = Looser.new
24
- u.public = '1'
25
- u.published_at = '2016-01-01'
26
- u.age = '23'
27
- end
28
- end
29
-
30
- Benchmark.ips do |x|
31
- x.report('SA create') do
32
- User.create!(public: '1', published_at: '2016-01-01', age: '23')
33
- end
34
-
35
- x.report('AR-T create') do
36
- Looser.create(public: '1', published_at: '2016-01-01', age: '23')
37
- end
38
- end
@@ -1,67 +0,0 @@
1
- begin
2
- require 'bundler/inline'
3
- rescue LoadError => e
4
- $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
5
- raise e
6
- end
7
-
8
- gemfile(true) do
9
- source 'https://rubygems.org'
10
- gem 'activerecord', '~>4.2'
11
- gem 'pg'
12
- gem 'activerecord-typedstore', require: false
13
- gem 'pry-byebug'
14
- gem 'benchmark-ips'
15
- gem 'memory_profiler'
16
- end
17
-
18
- DB_NAME = ENV['DB_NAME'] || 'sa_bench'
19
-
20
- begin
21
- system("createdb #{DB_NAME}")
22
- rescue
23
- $stdout.puts "DB already exists"
24
- end
25
-
26
- $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
27
-
28
- require 'active_record'
29
- require 'logger'
30
- require 'store_attribute'
31
- require 'activerecord-typedstore'
32
-
33
- ActiveRecord::Base.establish_connection(adapter: 'postgresql', database: DB_NAME)
34
-
35
- at_exit do
36
- ActiveRecord::Base.connection.disconnect!
37
- end
38
-
39
- module Bench
40
- module_function
41
- def setup_db
42
- ActiveRecord::Schema.define do
43
- create_table :users, force: true do |t|
44
- t.jsonb :data
45
- end
46
-
47
- create_table :loosers, force: true do |t|
48
- t.jsonb :data
49
- end
50
- end
51
- end
52
- end
53
-
54
- class User < ActiveRecord::Base
55
- store_accessor :data, public: :boolean, published_at: :datetime, age: :integer
56
- end
57
-
58
- class Looser < ActiveRecord::Base
59
- typed_store :data, coder: JSON do |s|
60
- s.boolean :public
61
- s.datetime :published_at
62
- s.integer :age
63
- end
64
- end
65
-
66
- # Run migration only if neccessary
67
- Bench.setup_db if ENV['FORCE'].present? || !ActiveRecord::Base.connection.tables.include?('users')
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "store_attribute"
5
-
6
- require "pry"
7
- Pry.start
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/bin/sh
2
-
3
- set -e
4
-
5
- gem install bundler --conservative
6
- bundle check || bundle install
7
-
8
- createdb store_attribute_test
@@ -1,5 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- gem 'rails', '~> 5.1.0'
4
-
5
- gemspec path: '..'
@@ -1,5 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- gem 'rails', '6.0.0.beta3'
4
-
5
- gemspec path: '..'
@@ -1,5 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- gem 'rails', github: 'rails/rails'
4
-
5
- gemspec path: '..'
@@ -1,173 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- describe StoreAttribute do
6
- before do
7
- @connection = ActiveRecord::Base.connection
8
-
9
- @connection.transaction do
10
- @connection.create_table("users") do |t|
11
- t.jsonb :jparams, default: {}, null: false
12
- t.text :custom
13
- t.hstore :hdata, default: {}, null: false
14
- end
15
- end
16
-
17
- User.reset_column_information
18
- end
19
-
20
- after do
21
- @connection.drop_table "users", if_exists: true
22
- end
23
-
24
- let(:time) { DateTime.new(2015, 2, 14, 17, 0, 0) }
25
- let(:time_str) { "2015-02-14 17:00" }
26
- let(:time_str_utc) { "2015-02-14 17:00:00 UTC" }
27
-
28
- context "hstore" do
29
- it "typecasts on build" do
30
- user = User.new(visible: "t", login_at: time_str)
31
- expect(user.visible).to eq true
32
- expect(user).to be_visible
33
- expect(user.login_at).to eq time
34
- end
35
-
36
- it "typecasts on reload" do
37
- user = User.new(visible: "t", login_at: time_str)
38
- user.save!
39
- user = User.find(user.id)
40
-
41
- expect(user.visible).to eq true
42
- expect(user).to be_visible
43
- expect(user.login_at).to eq time
44
- end
45
-
46
- it "works with accessors" do
47
- user = User.new
48
- user.visible = false
49
- user.login_at = time_str
50
- user.save!
51
-
52
- user = User.find(user.id)
53
-
54
- expect(user.visible).to be false
55
- expect(user).not_to be_visible
56
- expect(user.login_at).to eq time
57
-
58
- ron = RawUser.find(user.id)
59
- expect(ron.hdata["visible"]).to eq "false"
60
- expect(ron.hdata["login_at"]).to eq time_str_utc
61
- end
62
-
63
- it "handles options" do
64
- expect { User.create!(ratio: 1024) }.to raise_error(RangeError)
65
- end
66
-
67
- it "YAML roundtrip" do
68
- user = User.create!(visible: "0", login_at: time_str)
69
- dumped = YAML.load(YAML.dump(user)) # rubocop:disable Security/YAMLLoad
70
-
71
- expect(dumped.visible).to be false
72
- expect(dumped.login_at).to eq time
73
- end
74
- end
75
-
76
- context "jsonb" do
77
- it "typecasts on build" do
78
- jamie = User.new(
79
- active: "true",
80
- salary: 3.1999,
81
- birthday: "2000-01-01"
82
- )
83
- expect(jamie).to be_active
84
- expect(jamie.salary).to eq 3
85
- expect(jamie.birthday).to eq Date.new(2000, 1, 1)
86
- expect(jamie.jparams["birthday"]).to eq Date.new(2000, 1, 1)
87
- expect(jamie.jparams["active"]).to eq true
88
- end
89
-
90
- it "typecasts on reload" do
91
- jamie = User.create!(jparams: {"active" => "1", "birthday" => "01/01/2000", "salary" => "3.14"})
92
- jamie = User.find(jamie.id)
93
-
94
- expect(jamie).to be_active
95
- expect(jamie.salary).to eq 3
96
- expect(jamie.birthday).to eq Date.new(2000, 1, 1)
97
- expect(jamie.jparams["birthday"]).to eq Date.new(2000, 1, 1)
98
- expect(jamie.jparams["active"]).to eq true
99
- end
100
-
101
- it "works with accessors" do
102
- john = User.new
103
- john.active = 1
104
-
105
- expect(john).to be_active
106
- expect(john.jparams["active"]).to eq true
107
-
108
- john.jparams = {active: "true", salary: "123.123", birthday: "01/01/2012"}
109
- expect(john).to be_active
110
- expect(john.birthday).to eq Date.new(2012, 1, 1)
111
- expect(john.salary).to eq 123
112
-
113
- john.save!
114
-
115
- ron = RawUser.find(john.id)
116
- expect(ron.jparams["active"]).to eq true
117
- expect(ron.jparams["birthday"]).to eq "2012-01-01"
118
- expect(ron.jparams["salary"]).to eq 123
119
- end
120
-
121
- it "re-typecast old data" do
122
- jamie = User.create!
123
- User.update_all('jparams = \'{"active":"1", "salary":"12.02"}\'::jsonb')
124
-
125
- jamie = User.find(jamie.id)
126
- expect(jamie).to be_active
127
- expect(jamie.salary).to eq 12
128
-
129
- jamie.save!
130
-
131
- ron = RawUser.find(jamie.id)
132
- expect(ron.jparams["active"]).to eq true
133
- expect(ron.jparams["salary"]).to eq 12
134
- end
135
- end
136
-
137
- context "custom types" do
138
- it "typecasts on build" do
139
- user = User.new(price: "$1")
140
- expect(user.price).to eq 100
141
- end
142
-
143
- it "typecasts on reload" do
144
- jamie = User.create!(custom: {price: "$12"})
145
- expect(jamie.reload.price).to eq 1200
146
-
147
- jamie = User.find(jamie.id)
148
-
149
- expect(jamie.price).to eq 1200
150
- end
151
- end
152
-
153
- context "store subtype" do
154
- it "typecasts on build" do
155
- user = User.new(inner_json: {x: 1})
156
- expect(user.inner_json).to eq("x" => 1)
157
- end
158
-
159
- it "typecasts on update" do
160
- user = User.new
161
- user.update!(inner_json: {x: 1})
162
- expect(user.inner_json).to eq("x" => 1)
163
-
164
- expect(user.reload.inner_json).to eq("x" => 1)
165
- end
166
-
167
- it "typecasts on reload" do
168
- jamie = User.create!(inner_json: {x: 1})
169
- jamie = User.find(jamie.id)
170
- expect(jamie.inner_json).to eq("x" => 1)
171
- end
172
- end
173
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
4
- $LOAD_PATH.unshift(File.dirname(__FILE__))
5
-
6
- if ENV["COVER"]
7
- require "simplecov"
8
- SimpleCov.root File.join(File.dirname(__FILE__), "..")
9
- SimpleCov.start
10
- end
11
-
12
- require "rspec"
13
- require "pry-byebug"
14
- require "active_record"
15
- require "pg"
16
- require "store_attribute"
17
-
18
- RAILS_5_1 = ActiveRecord.version.release >= Gem::Version.new("5.1.0")
19
-
20
- ActiveRecord::Base.establish_connection(
21
- adapter: "postgresql",
22
- database: "store_attribute_test"
23
- )
24
- connection = ActiveRecord::Base.connection
25
-
26
- unless connection.extension_enabled?("hstore")
27
- connection.enable_extension "hstore"
28
- connection.commit_db_transaction
29
- end
30
-
31
- connection.reconnect!
32
-
33
- Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
34
-
35
- RSpec.configure do |config|
36
- config.mock_with :rspec
37
-
38
- config.filter_run_when_matching :focus
39
-
40
- config.example_status_persistence_file_path = "tmp/rspec_examples.txt"
41
-
42
- if config.files_to_run.one?
43
- config.default_formatter = "doc"
44
- end
45
- end
@@ -1,109 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- describe ActiveRecord::Type::TypedStore do
6
- let(:json_type) { ActiveRecord::Type::Serialized.new(ActiveRecord::Type::Text.new, ActiveRecord::Coders::JSON) }
7
-
8
- context "with json store" do
9
- subject { described_class.new(json_type) }
10
-
11
- describe "#cast" do
12
- it "without key types", :aggregate_failures do
13
- expect(subject.cast([1, 2])).to eq [1, 2]
14
- expect(subject.cast("a" => "b")).to eq("a" => "b")
15
- end
16
-
17
- it "with type keys" do
18
- subject.add_typed_key("date", :date)
19
-
20
- date = ::Date.new(2016, 6, 22)
21
- expect(subject.cast(date: "2016-06-22")).to eq("date" => date)
22
- end
23
- end
24
-
25
- describe "#deserialize" do
26
- it "without key types", :aggregate_failures do
27
- expect(subject.deserialize("[1,2]")).to eq [1, 2]
28
- expect(subject.deserialize('{"a":"b"}')).to eq("a" => "b")
29
- end
30
-
31
- it "with type keys" do
32
- subject.add_typed_key("date", :date)
33
-
34
- date = ::Date.new(2016, 6, 22)
35
- expect(subject.deserialize('{"date":"2016-06-22"}')).to eq("date" => date)
36
- end
37
- end
38
-
39
- describe "#serialize" do
40
- it "without key types", :aggregate_failures do
41
- expect(subject.serialize([1, 2])).to eq "[1,2]"
42
- expect(subject.serialize("a" => "b")).to eq '{"a":"b"}'
43
- end
44
-
45
- it "with type keys" do
46
- subject.add_typed_key("date", :date)
47
-
48
- date = ::Date.new(2016, 6, 22)
49
- expect(subject.serialize(date: date)).to eq '{"date":"2016-06-22"}'
50
- end
51
-
52
- it "with type key with option" do
53
- subject.add_typed_key("val", :integer, limit: 1)
54
-
55
- expect { subject.serialize(val: 1024) }.to raise_error(RangeError)
56
- end
57
- end
58
-
59
- describe ".create_from_type" do
60
- it "creates with valid types", :aggregate_failures do
61
- type = described_class.create_from_type(json_type, "date", :date)
62
- new_type = described_class.create_from_type(type, "val", :integer)
63
-
64
- date = ::Date.new(2016, 6, 22)
65
-
66
- expect(type.cast(date: "2016-06-22", val: "1.2")).to eq("date" => date, "val" => "1.2")
67
- expect(new_type.cast(date: "2016-06-22", val: "1.2")).to eq("date" => date, "val" => 1)
68
- end
69
- end
70
- end
71
-
72
- context "with yaml coder" do
73
- if RAILS_5_1
74
- let(:yaml_type) do
75
- ActiveRecord::Type::Serialized.new(
76
- ActiveRecord::Type::Text.new,
77
- ActiveRecord::Store::IndifferentCoder.new(
78
- "test",
79
- ActiveRecord::Coders::YAMLColumn.new("test", Hash)
80
- )
81
- )
82
- end
83
- else
84
- let(:yaml_type) do
85
- ActiveRecord::Type::Serialized.new(
86
- ActiveRecord::Type::Text.new,
87
- ActiveRecord::Store::IndifferentCoder.new(
88
- ActiveRecord::Coders::YAMLColumn.new(Hash)
89
- )
90
- )
91
- end
92
- end
93
-
94
- let(:subject) { described_class.new(yaml_type) }
95
-
96
- it "works", :aggregate_failures do
97
- subject.add_typed_key("date", :date)
98
-
99
- date = ::Date.new(2016, 6, 22)
100
-
101
- expect(subject.cast(date: "2016-06-22")).to eq("date" => date)
102
- expect(subject.cast("date" => "2016-06-22")).to eq("date" => date)
103
- expect(subject.deserialize("---\n:date: 2016-06-22\n")).to eq("date" => date)
104
- expect(subject.deserialize("---\ndate: 2016-06-22\n")).to eq("date" => date)
105
- expect(subject.serialize(date: date)).to eq "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\ndate: 2016-06-22\n"
106
- expect(subject.serialize("date" => date)).to eq "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\ndate: 2016-06-22\n"
107
- end
108
- end
109
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class MoneyType < ActiveRecord::Type::Integer
4
- def cast(value)
5
- if !value.is_a?(Numeric) && value.include?("$")
6
- price_in_dollars = value.delete("$").to_f
7
- super(price_in_dollars * 100)
8
- else
9
- super
10
- end
11
- end
12
- end
13
-
14
- ActiveRecord::Type.register(:money_type, MoneyType)
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class RawUser < ActiveRecord::Base
4
- self.table_name = "users"
5
- end
6
-
7
- class User < ActiveRecord::Base
8
- store_accessor :jparams, :version, active: :boolean, salary: :integer
9
- store_attribute :jparams, :birthday, :date
10
-
11
- store_attribute :jparams, :inner_json, :json
12
-
13
- store :custom, accessors: [price: :money_type]
14
-
15
- store_accessor :hdata, visible: :boolean
16
-
17
- store_attribute :hdata, :ratio, :integer, limit: 1
18
- store_attribute :hdata, :login_at, :datetime
19
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- $:.push File.expand_path("../lib", __FILE__)
4
-
5
- # Maintain your gem's version:
6
- require "store_attribute/version"
7
-
8
- # Describe your gem and declare its dependencies:
9
- Gem::Specification.new do |s|
10
- s.name = "store_attribute"
11
- s.version = StoreAttribute::VERSION
12
- s.authors = ["palkan"]
13
- s.email = ["dementiev.vm@gmail.com"]
14
- s.homepage = "http://github.com/palkan/store_attribute"
15
- s.summary = "ActiveRecord extension which adds typecasting to store accessors"
16
- s.description = "ActiveRecord extension which adds typecasting to store accessors"
17
- s.license = "MIT"
18
-
19
- s.files = `git ls-files`.split($/)
20
- s.require_paths = ["lib"]
21
-
22
- s.required_ruby_version = ">= 2.4.0"
23
-
24
- s.add_runtime_dependency "activerecord", ">= 5.0"
25
-
26
- s.add_development_dependency "pg", ">= 0.18"
27
- s.add_development_dependency "rake", ">= 10.1"
28
- s.add_development_dependency "simplecov", ">= 0.3.8"
29
- s.add_development_dependency "pry-byebug"
30
- s.add_development_dependency "rubocop", "~> 0.65.0"
31
- s.add_development_dependency "rubocop-md", "~> 0.2"
32
- s.add_development_dependency "standard", "~> 0.0.36"
33
- s.add_development_dependency "rspec", ">= 3.5.0"
34
- end