store_attribute 0.5.1 → 0.7.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
- SHA1:
3
- metadata.gz: cd2643cf4b1909497a0de3f0d15ed6ca51ba599e
4
- data.tar.gz: 607171026ea9b9638e20a920b732bd7258db246a
2
+ SHA256:
3
+ metadata.gz: 941970a7a98bfa915d206d2fde758075a81bc610109426e0eaa74859d81bfde7
4
+ data.tar.gz: 3bb9f5fa94b6f9f321006172e9f8cb4a4e7c9ba7030364f8468c829989a3f5d7
5
5
  SHA512:
6
- metadata.gz: 79e570bd455ecf0f2f7c500725e4a243f4e0d5133c5bdfd45100e10963bbe16623c37396d4a87b7018c74472813efbcfd212cc9d7ebe83c697ca00ad053d338c
7
- data.tar.gz: 0c4458f32f0a658d276218894912e3c1c53833dd0cb4e506885bcff00a824beeadda620c89a4d6c09376edd1a262b9396ad317eb275c981d4d08194eb48968da
6
+ metadata.gz: ac9e82f2c78173e0054ea97f1c3d1318fd36b671b531b71d3f1e73be96a3247d7565b43cd27b21d3c2f01b6585143eeddb75a9cec99ef43cd77e0d43a3035130
7
+ data.tar.gz: edd8234b7dc2c704d8f3086e29641a9ff1fa866f6266341d71c6f566dfb8f72fed3d360b972b0c63c41a5c0a383af645655d75d8beabb731521ab97f4b38a8da
@@ -0,0 +1,29 @@
1
+ # Change log
2
+
3
+ ## master
4
+
5
+ ## 0.7.1
6
+
7
+ - Fixed bug with `store` called without accessors. ([@ioki-klaus][])
8
+
9
+ See [#10](https://github.com/palkan/store_attribute/pull/10).
10
+
11
+ ## 0.7.0 (2020-03-23)
12
+
13
+ - Added dirty tracking methods. ([@glaszig][])
14
+
15
+ [PR #8](https://github.com/palkan/store_attribute/pull/8).
16
+
17
+ ## 0.6.0 (2019-07-24)
18
+
19
+ - Added default values support. ([@dreikanter][], [@SumLare][])
20
+
21
+ See [PR #7](https://github.com/palkan/store_attribute/pull/7).
22
+
23
+ - Start keeping changelog. ([@palkan][])
24
+
25
+ [@palkan]: https://github.com/palkan
26
+ [@dreikanter]: https://github.com/dreikanter
27
+ [@SumLare]: https://github.com/SumLare
28
+ [@glaszig]: https://github.com/glaszig
29
+ [@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
@@ -4,21 +4,20 @@
4
4
 
5
5
  ActiveRecord extension which adds typecasting to store accessors.
6
6
 
7
- Compatible with Rails 4.2 and Rails 5.
7
+ Compatible with Rails 4.2 and Rails 5+.
8
8
 
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>
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
 
14
13
  In your Gemfile:
15
14
 
16
15
  ```ruby
17
- # for Rails 5
18
- gem "store_attribute", "~>0.5.0"
16
+ # for Rails 5+ (6 is supported)
17
+ gem "store_attribute", "~> 0.5.0"
19
18
 
20
19
  # for Rails 4.2
21
- gem "store_attribute", "~>0.4.0"
20
+ gem "store_attribute", "~> 0.4.0"
22
21
  ```
23
22
 
24
23
  ### Usage
@@ -26,14 +25,14 @@ gem "store_attribute", "~>0.4.0"
26
25
  You can use `store_attribute` method to add additional accessors with a type to an existing store on a model.
27
26
 
28
27
  ```ruby
29
- store_attribute(store_name, name, type, options = {})
28
+ store_attribute(store_name, name, type, options)
30
29
  ```
31
30
 
32
31
  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` 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,30 +46,36 @@ 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
- u = MegaUser.new(active: false, login_at: '2015-01-01 00:01', ratio: "63.4608")
53
+ u = MegaUser.new(active: false, login_at: "2015-01-01 00:01", ratio: "63.4608")
53
54
 
54
55
  u.login_at.is_a?(DateTime) # => true
55
- u.login_at = DateTime.new(2015,1,1,11,0,0)
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
61
66
 
62
67
  # After loading record from db store contains casted data
63
- u.settings['login_at'] == DateTime.new(2015,1,1,11,0,0) # => true
68
+ u.settings["login_at"] == DateTime.new(2015, 1, 1, 11, 0, 0) # => true
64
69
 
65
70
  # If you update store explicitly then the value returned
66
71
  # by accessor isn't type casted
67
- u.settings['ration'] = "3.141592653"
72
+ u.settings["ratio"] = "3.141592653"
68
73
  u.ratio # => "3.141592653"
69
74
 
70
75
  # On the other hand, writing through accessor set correct data within store
71
76
  u.ratio = "3.141592653"
72
77
  u.ratio # => 3
73
- u.settings['ratio'] # => 3
78
+ u.settings["ratio"] # => 3
74
79
  ```
75
80
 
76
81
  You can also specify type using usual `store_accessor` method:
@@ -1,2 +1,4 @@
1
- require 'store_attribute/version'
2
- require 'store_attribute/active_record'
1
+ # frozen_string_literal: true
2
+
3
+ require "store_attribute/version"
4
+ require "store_attribute/active_record"
@@ -1 +1,3 @@
1
- require 'store_attribute/active_record/store'
1
+ # frozen_string_literal: true
2
+
3
+ require "store_attribute/active_record/store"
@@ -1,9 +1,12 @@
1
- require 'active_record/store'
2
- require 'store_attribute/active_record/type/typed_store'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/store"
4
+ require "store_attribute/active_record/type/typed_store"
3
5
 
4
6
  module ActiveRecord
5
7
  module Store
6
8
  module ClassMethods # :nodoc:
9
+ alias _orig_store store
7
10
  # Defines store on this model.
8
11
  #
9
12
  # +store_name+ The name of the store.
@@ -21,9 +24,18 @@ module ActiveRecord
21
24
  # store :settings, accessors: [:color, :homepage, login_at: :datetime], coder: JSON
22
25
  # end
23
26
  def store(store_name, options = {})
24
- serialize store_name, IndifferentCoder.new(options[:coder])
25
- store_accessor(store_name, *options[:accessors]) if options.key?(:accessors)
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
+
35
+ _orig_store(store_name, options)
36
+ store_accessor(store_name, *accessors, **typed_accessors) if accessors
26
37
  end
38
+
27
39
  # Adds additional accessors to an existing store on this model.
28
40
  #
29
41
  # +store_name+ The name of the store.
@@ -43,6 +55,9 @@ module ActiveRecord
43
55
 
44
56
  _define_accessors_methods(store_name, *keys)
45
57
 
58
+ _define_dirty_tracking_methods(store_name, keys)
59
+ _define_dirty_tracking_methods(store_name, typed_keys.keys)
60
+
46
61
  _prepare_local_stored_attributes(store_name, *keys)
47
62
 
48
63
  typed_keys.each do |key, type|
@@ -134,6 +149,50 @@ module ActiveRecord
134
149
  end
135
150
  end
136
151
  end
152
+
153
+ def _define_dirty_tracking_methods(store_attribute, keys)
154
+ _store_accessors_module.module_eval do
155
+ keys.flatten.each do |key|
156
+ key = key.to_s
157
+
158
+ define_method("#{key}_changed?") do
159
+ return false unless attribute_changed?(store_attribute)
160
+ prev_store, new_store = changes[store_attribute]
161
+ prev_store&.dig(key) != new_store&.dig(key)
162
+ end
163
+
164
+ define_method("#{key}_change") do
165
+ return unless attribute_changed?(store_attribute)
166
+ prev_store, new_store = changes[store_attribute]
167
+ [prev_store&.dig(key), new_store&.dig(key)]
168
+ end
169
+
170
+ define_method("#{key}_was") do
171
+ return unless attribute_changed?(store_attribute)
172
+ prev_store, _new_store = changes[store_attribute]
173
+ prev_store&.dig(key)
174
+ end
175
+
176
+ define_method("saved_change_to_#{key}?") do
177
+ return false unless saved_change_to_attribute?(store_attribute)
178
+ prev_store, new_store = saved_change_to_attribute(store_attribute)
179
+ prev_store&.dig(key) != new_store&.dig(key)
180
+ end
181
+
182
+ define_method("saved_change_to_#{key}") do
183
+ return unless saved_change_to_attribute?(store_attribute)
184
+ prev_store, new_store = saved_change_to_attribute(store_attribute)
185
+ [prev_store&.dig(key), new_store&.dig(key)]
186
+ end
187
+
188
+ define_method("#{key}_before_last_save") do
189
+ return unless saved_change_to_attribute?(store_attribute)
190
+ prev_store, _new_store = saved_change_to_attribute(store_attribute)
191
+ prev_store&.dig(key)
192
+ end
193
+ end
194
+ end
195
+ end
137
196
  end
138
197
  end
139
198
  end
@@ -1,4 +1,6 @@
1
- require 'active_record/type'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/type"
2
4
 
3
5
  module ActiveRecord
4
6
  module Type # :nodoc:
@@ -13,43 +15,57 @@ module ActiveRecord
13
15
 
14
16
  def initialize(subtype)
15
17
  @accessor_types = {}
18
+ @defaults = {}
16
19
  @store_accessor = subtype.accessor
17
20
  super(subtype)
18
21
  end
19
22
 
20
- def add_typed_key(key, type, **options)
21
- type = ActiveRecord::Type.lookup(type, options) if type.is_a?(Symbol)
22
- @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
23
31
  end
24
32
 
25
33
  def deserialize(value)
26
34
  hash = super
27
- if hash
28
- accessor_types.each do |key, type|
29
- 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)
30
41
  end
31
42
  end
32
43
  hash
33
44
  end
34
45
 
35
46
  def serialize(value)
36
- if value.is_a?(Hash)
37
- typed_casted = {}
38
- accessor_types.each do |key, type|
39
- k = key_to_cast(value, key)
40
- 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))
41
56
  end
42
- super(value.merge(typed_casted))
43
- else
44
- super(value)
45
57
  end
58
+ super(value.merge(typed_casted))
46
59
  end
47
60
 
48
61
  def cast(value)
49
62
  hash = super
50
- if hash
51
- accessor_types.each do |key, type|
52
- 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)
53
69
  end
54
70
  end
55
71
  hash
@@ -72,6 +88,7 @@ module ActiveRecord
72
88
  def key_to_cast(val, key)
73
89
  return key if val.key?(key)
74
90
  return key.to_sym if val.key?(key.to_sym)
91
+ return key if defaults.key?(key)
75
92
  end
76
93
 
77
94
  def typed?(key)
@@ -82,7 +99,12 @@ module ActiveRecord
82
99
  accessor_types.fetch(key.to_s)
83
100
  end
84
101
 
85
- 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
86
108
  end
87
109
  end
88
110
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StoreAttribute # :nodoc:
2
- VERSION = "0.5.1".freeze
4
+ VERSION = "0.7.1"
3
5
  end
metadata CHANGED
@@ -1,97 +1,97 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: store_attribute
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-07-08 00:00:00.000000000 Z
11
+ date: 2020-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 5.0.0
19
+ version: '5.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 5.0.0
26
+ version: '5.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: pg
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0.18'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.18'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
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'
54
+ version: '13.0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: simplecov
56
+ name: rubocop-md
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ">="
59
+ - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 0.3.8
61
+ version: '0.3'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ">="
66
+ - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 0.3.8
68
+ version: '0.3'
69
69
  - !ruby/object:Gem::Dependency
70
- name: pry-byebug
70
+ name: standard
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ">="
73
+ - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '0'
75
+ version: 0.2.0
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ">="
80
+ - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '0'
82
+ version: 0.2.0
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: rspec
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - "~>"
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
89
  version: 3.5.0
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - "~>"
94
+ - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: 3.5.0
97
97
  description: ActiveRecord extension which adds typecasting to store accessors
@@ -101,34 +101,23 @@ executables: []
101
101
  extensions: []
102
102
  extra_rdoc_files: []
103
103
  files:
104
- - ".gitignore"
105
- - ".rspec"
106
- - ".rubocop.yml"
107
- - ".travis.yml"
108
- - Gemfile
109
- - MIT-LICENSE
104
+ - CHANGELOG.md
105
+ - LICENSE.txt
110
106
  - README.md
111
- - Rakefile
112
- - bench/bench.rb
113
- - bench/setup.rb
114
- - bin/console
115
- - bin/setup
116
- - gemfiles/rails5.gemfile
117
107
  - lib/store_attribute.rb
118
108
  - lib/store_attribute/active_record.rb
119
109
  - lib/store_attribute/active_record/store.rb
120
110
  - lib/store_attribute/active_record/type/typed_store.rb
121
111
  - lib/store_attribute/version.rb
122
- - spec/cases/store_attribute_spec.rb
123
- - spec/spec_helper.rb
124
- - spec/store_attribute/typed_store_spec.rb
125
- - spec/support/money_type.rb
126
- - spec/support/user.rb
127
- - store_attribute.gemspec
128
112
  homepage: http://github.com/palkan/store_attribute
129
113
  licenses:
130
114
  - MIT
131
- 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
132
121
  post_install_message:
133
122
  rdoc_options: []
134
123
  require_paths:
@@ -137,15 +126,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
137
126
  requirements:
138
127
  - - ">="
139
128
  - !ruby/object:Gem::Version
140
- version: '0'
129
+ version: 2.4.0
141
130
  required_rubygems_version: !ruby/object:Gem::Requirement
142
131
  requirements:
143
132
  - - ">="
144
133
  - !ruby/object:Gem::Version
145
134
  version: '0'
146
135
  requirements: []
147
- rubyforge_project:
148
- rubygems_version: 2.6.4
136
+ rubygems_version: 3.0.6
149
137
  signing_key:
150
138
  specification_version: 4
151
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,51 +0,0 @@
1
- AllCops:
2
- # Include gemspec and Rakefile
3
- Include:
4
- - 'lib/**/*.rb'
5
- - 'lib/**/*.rake'
6
- - 'spec/**/*.rb'
7
- Exclude:
8
- - 'bin/**/*'
9
- - 'spec/dummy/**/*'
10
- - 'tmp/**/*'
11
- - 'bench/**/*'
12
- DisplayCopNames: true
13
- StyleGuideCopsOnly: false
14
-
15
- Style/AccessorMethodName:
16
- Enabled: false
17
-
18
- Style/TrivialAccessors:
19
- Enabled: false
20
-
21
- Style/Documentation:
22
- Exclude:
23
- - 'spec/**/*.rb'
24
-
25
- Style/StringLiterals:
26
- Enabled: false
27
-
28
- Style/SpaceInsideStringInterpolation:
29
- EnforcedStyle: no_space
30
-
31
- Style/BlockDelimiters:
32
- Exclude:
33
- - 'spec/**/*.rb'
34
-
35
- Lint/AmbiguousRegexpLiteral:
36
- Enabled: false
37
-
38
- Metrics/MethodLength:
39
- Exclude:
40
- - 'spec/**/*.rb'
41
-
42
- Metrics/LineLength:
43
- Max: 100
44
- Exclude:
45
- - 'spec/**/*.rb'
46
-
47
- Rails/Date:
48
- Enabled: false
49
-
50
- Rails/TimeZone:
51
- Enabled: false
@@ -1,14 +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.3.1
14
- gemfile: gemfiles/rails5.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.0.0'
11
- end
data/Rakefile DELETED
@@ -1,6 +0,0 @@
1
- require "bundler/gem_tasks"
2
-
3
- require 'rspec/core/rake_task'
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task :default => :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.0.0'
4
-
5
- gemspec path: '..'
@@ -1,171 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe StoreAttribute do
4
- before do
5
- @connection = ActiveRecord::Base.connection
6
-
7
- @connection.transaction do
8
- @connection.create_table('users') do |t|
9
- t.jsonb :jparams, default: {}, null: false
10
- t.text :custom
11
- t.hstore :hdata, default: {}, null: false
12
- end
13
- end
14
-
15
- User.reset_column_information
16
- end
17
-
18
- after do
19
- @connection.drop_table 'users', if_exists: true
20
- end
21
-
22
- let(:time) { DateTime.new(2015, 2, 14, 17, 0, 0) }
23
- let(:time_str) { '2015-02-14 17:00' }
24
- let(:time_str_utc) { '2015-02-14 17:00:00 UTC' }
25
-
26
- context "hstore" do
27
- it "typecasts on build" do
28
- user = User.new(visible: 't', login_at: time_str)
29
- expect(user.visible).to eq true
30
- expect(user).to be_visible
31
- expect(user.login_at).to eq time
32
- end
33
-
34
- it "typecasts on reload" do
35
- user = User.new(visible: 't', login_at: time_str)
36
- user.save!
37
- user = User.find(user.id)
38
-
39
- expect(user.visible).to eq true
40
- expect(user).to be_visible
41
- expect(user.login_at).to eq time
42
- end
43
-
44
- it "works with accessors" do
45
- user = User.new
46
- user.visible = false
47
- user.login_at = time_str
48
- user.save!
49
-
50
- user = User.find(user.id)
51
-
52
- expect(user.visible).to be false
53
- expect(user).not_to be_visible
54
- expect(user.login_at).to eq time
55
-
56
- ron = RawUser.find(user.id)
57
- expect(ron.hdata['visible']).to eq 'false'
58
- expect(ron.hdata['login_at']).to eq time_str_utc
59
- end
60
-
61
- it "handles options" do
62
- expect { User.create!(ratio: 1024) }.to raise_error(RangeError)
63
- end
64
-
65
- it "YAML roundtrip" do
66
- user = User.create!(visible: '0', login_at: time_str)
67
- dumped = YAML.load(YAML.dump(user))
68
-
69
- expect(dumped.visible).to be false
70
- expect(dumped.login_at).to eq time
71
- end
72
- end
73
-
74
- context "jsonb" do
75
- it "typecasts on build" do
76
- jamie = User.new(
77
- active: 'true',
78
- salary: 3.1999,
79
- birthday: '2000-01-01'
80
- )
81
- expect(jamie).to be_active
82
- expect(jamie.salary).to eq 3
83
- expect(jamie.birthday).to eq Date.new(2000, 1, 1)
84
- expect(jamie.jparams['birthday']).to eq Date.new(2000, 1, 1)
85
- expect(jamie.jparams['active']).to eq true
86
- end
87
-
88
- it "typecasts on reload" do
89
- jamie = User.create!(jparams: { 'active' => '1', 'birthday' => '01/01/2000', 'salary' => '3.14' })
90
- jamie = User.find(jamie.id)
91
-
92
- expect(jamie).to be_active
93
- expect(jamie.salary).to eq 3
94
- expect(jamie.birthday).to eq Date.new(2000, 1, 1)
95
- expect(jamie.jparams['birthday']).to eq Date.new(2000, 1, 1)
96
- expect(jamie.jparams['active']).to eq true
97
- end
98
-
99
- it "works with accessors" do
100
- john = User.new
101
- john.active = 1
102
-
103
- expect(john).to be_active
104
- expect(john.jparams['active']).to eq true
105
-
106
- john.jparams = { active: 'true', salary: '123.123', birthday: '01/01/2012' }
107
- expect(john).to be_active
108
- expect(john.birthday).to eq Date.new(2012, 1, 1)
109
- expect(john.salary).to eq 123
110
-
111
- john.save!
112
-
113
- ron = RawUser.find(john.id)
114
- expect(ron.jparams['active']).to eq true
115
- expect(ron.jparams['birthday']).to eq '2012-01-01'
116
- expect(ron.jparams['salary']).to eq 123
117
- end
118
-
119
- it "re-typecast old data" do
120
- jamie = User.create!
121
- User.update_all('jparams = \'{"active":"1", "salary":"12.02"}\'::jsonb')
122
-
123
- jamie = User.find(jamie.id)
124
- expect(jamie).to be_active
125
- expect(jamie.salary).to eq 12
126
-
127
- jamie.save!
128
-
129
- ron = RawUser.find(jamie.id)
130
- expect(ron.jparams['active']).to eq true
131
- expect(ron.jparams['salary']).to eq 12
132
- end
133
- end
134
-
135
- context "custom types" do
136
- it "typecasts on build" do
137
- user = User.new(price: "$1")
138
- expect(user.price).to eq 100
139
- end
140
-
141
- it "typecasts on reload" do
142
- jamie = User.create!(custom: { price: '$12' })
143
- expect(jamie.reload.price).to eq 1200
144
-
145
- jamie = User.find(jamie.id)
146
-
147
- expect(jamie.price).to eq 1200
148
- end
149
- end
150
-
151
- context "store subtype" do
152
- it "typecasts on build" do
153
- user = User.new(inner_json: { x: 1 })
154
- expect(user.inner_json).to eq('x' => 1)
155
- end
156
-
157
- it "typecasts on update" do
158
- user = User.new
159
- user.update!(inner_json: { x: 1 })
160
- expect(user.inner_json).to eq('x' => 1)
161
-
162
- expect(user.reload.inner_json).to eq('x' => 1)
163
- end
164
-
165
- it "typecasts on reload" do
166
- jamie = User.create!(inner_json: { x: 1 })
167
- jamie = User.find(jamie.id)
168
- expect(jamie.inner_json).to eq('x' => 1)
169
- end
170
- end
171
- end
@@ -1,41 +0,0 @@
1
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
- $LOAD_PATH.unshift(File.dirname(__FILE__))
3
-
4
- if ENV['COVER']
5
- require 'simplecov'
6
- SimpleCov.root File.join(File.dirname(__FILE__), '..')
7
- SimpleCov.start
8
- end
9
-
10
- require 'rspec'
11
- require 'pry-byebug'
12
- require 'active_record'
13
- require 'pg'
14
- require 'store_attribute'
15
-
16
- ActiveRecord::Base.establish_connection(
17
- adapter: 'postgresql',
18
- database: 'store_attribute_test'
19
- )
20
- connection = ActiveRecord::Base.connection
21
-
22
- unless connection.extension_enabled?('hstore')
23
- connection.enable_extension 'hstore'
24
- connection.commit_db_transaction
25
- end
26
-
27
- connection.reconnect!
28
-
29
- Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
30
-
31
- RSpec.configure do |config|
32
- config.mock_with :rspec
33
-
34
- config.filter_run_when_matching :focus
35
-
36
- config.example_status_persistence_file_path = "tmp/rspec_examples.txt"
37
-
38
- if config.files_to_run.one?
39
- config.default_formatter = 'doc'
40
- end
41
- end
@@ -1,94 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe ActiveRecord::Type::TypedStore do
4
- let(:json_type) { ActiveRecord::Type::Serialized.new(ActiveRecord::Type::Text.new, ActiveRecord::Coders::JSON) }
5
- let(:yaml_type) do
6
- ActiveRecord::Type::Serialized.new(
7
- ActiveRecord::Type::Text.new,
8
- ActiveRecord::Store::IndifferentCoder.new(
9
- ActiveRecord::Coders::YAMLColumn.new(Hash)
10
- )
11
- )
12
- end
13
-
14
- context "with json store" do
15
- subject { described_class.new(json_type) }
16
-
17
- describe "#cast" do
18
- it "without key types", :aggregate_failures do
19
- expect(subject.cast([1, 2])).to eq [1, 2]
20
- expect(subject.cast('a' => 'b')).to eq('a' => 'b')
21
- end
22
-
23
- it "with type keys" do
24
- subject.add_typed_key('date', :date)
25
-
26
- date = ::Date.new(2016, 6, 22)
27
- expect(subject.cast(date: '2016-06-22')).to eq('date' => date)
28
- end
29
- end
30
-
31
- describe "#deserialize" do
32
- it "without key types", :aggregate_failures do
33
- expect(subject.deserialize('[1,2]')).to eq [1, 2]
34
- expect(subject.deserialize('{"a":"b"}')).to eq('a' => 'b')
35
- end
36
-
37
- it "with type keys" do
38
- subject.add_typed_key('date', :date)
39
-
40
- date = ::Date.new(2016, 6, 22)
41
- expect(subject.deserialize('{"date":"2016-06-22"}')).to eq('date' => date)
42
- end
43
- end
44
-
45
- describe "#serialize" do
46
- it "without key types", :aggregate_failures do
47
- expect(subject.serialize([1, 2])).to eq '[1,2]'
48
- expect(subject.serialize('a' => 'b')).to eq '{"a":"b"}'
49
- end
50
-
51
- it "with type keys" do
52
- subject.add_typed_key('date', :date)
53
-
54
- date = ::Date.new(2016, 6, 22)
55
- expect(subject.serialize(date: date)).to eq '{"date":"2016-06-22"}'
56
- end
57
-
58
- it "with type key with option" do
59
- subject.add_typed_key('val', :integer, limit: 1)
60
-
61
- expect { subject.serialize(val: 1024) }.to raise_error(RangeError)
62
- end
63
- end
64
-
65
- describe ".create_from_type" do
66
- it "creates with valid types", :aggregate_failures do
67
- type = described_class.create_from_type(json_type, 'date', :date)
68
- new_type = described_class.create_from_type(type, 'val', :integer)
69
-
70
- date = ::Date.new(2016, 6, 22)
71
-
72
- expect(type.cast(date: '2016-06-22', val: '1.2')).to eq('date' => date, 'val' => '1.2')
73
- expect(new_type.cast(date: '2016-06-22', val: '1.2')).to eq('date' => date, 'val' => 1)
74
- end
75
- end
76
- end
77
-
78
- context "with yaml coder" do
79
- let(:subject) { described_class.new(yaml_type) }
80
-
81
- it "works", :aggregate_failures do
82
- subject.add_typed_key('date', :date)
83
-
84
- date = ::Date.new(2016, 6, 22)
85
-
86
- expect(subject.cast(date: '2016-06-22')).to eq('date' => date)
87
- expect(subject.cast('date' => '2016-06-22')).to eq('date' => date)
88
- expect(subject.deserialize("---\n:date: 2016-06-22\n")).to eq('date' => date)
89
- expect(subject.deserialize("---\ndate: 2016-06-22\n")).to eq('date' => date)
90
- expect(subject.serialize(date: date)).to eq "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\ndate: 2016-06-22\n"
91
- expect(subject.serialize('date' => date)).to eq "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\ndate: 2016-06-22\n"
92
- end
93
- end
94
- end
@@ -1,12 +0,0 @@
1
- class MoneyType < ActiveRecord::Type::Integer
2
- def cast(value)
3
- if !value.is_a?(Numeric) && value.include?('$')
4
- price_in_dollars = value.delete('$').to_f
5
- super(price_in_dollars * 100)
6
- else
7
- super
8
- end
9
- end
10
- end
11
-
12
- ActiveRecord::Type.register(:money_type, MoneyType)
@@ -1,17 +0,0 @@
1
- class RawUser < ActiveRecord::Base
2
- self.table_name = 'users'
3
- end
4
-
5
- class User < ActiveRecord::Base
6
- store_accessor :jparams, :version, active: :boolean, salary: :integer
7
- store_attribute :jparams, :birthday, :date
8
-
9
- store_attribute :jparams, :inner_json, :json
10
-
11
- store :custom, accessors: [price: :money_type]
12
-
13
- store_accessor :hdata, visible: :boolean
14
-
15
- store_attribute :hdata, :ratio, :integer, limit: 1
16
- store_attribute :hdata, :login_at, :datetime
17
- end
@@ -1,27 +0,0 @@
1
- $:.push File.expand_path("../lib", __FILE__)
2
-
3
- # Maintain your gem's version:
4
- require "store_attribute/version"
5
-
6
- # Describe your gem and declare its dependencies:
7
- Gem::Specification.new do |s|
8
- s.name = "store_attribute"
9
- s.version = StoreAttribute::VERSION
10
- s.authors = ["palkan"]
11
- s.email = ["dementiev.vm@gmail.com"]
12
- s.homepage = "http://github.com/palkan/store_attribute"
13
- s.summary = "ActiveRecord extension which adds typecasting to store accessors"
14
- s.description = "ActiveRecord extension which adds typecasting to store accessors"
15
- s.license = "MIT"
16
-
17
- s.files = `git ls-files`.split($/)
18
- s.require_paths = ["lib"]
19
-
20
- s.add_runtime_dependency "activerecord", "~>5.0.0"
21
-
22
- s.add_development_dependency "pg", "~>0.18"
23
- s.add_development_dependency "rake", "~> 10.1"
24
- s.add_development_dependency "simplecov", ">= 0.3.8"
25
- s.add_development_dependency "pry-byebug"
26
- s.add_development_dependency "rspec", "~> 3.5.0"
27
- end