store_attribute 0.5.3 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
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