store_attribute 0.4.0
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 +7 -0
- data/.gitignore +36 -0
- data/.rspec +1 -0
- data/.rubocop.yml +51 -0
- data/.travis.yml +14 -0
- data/Gemfile +11 -0
- data/MIT-LICENSE +20 -0
- data/README.md +84 -0
- data/Rakefile +6 -0
- data/bench/bench.rb +38 -0
- data/bench/setup.rb +67 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/gemfiles/rails-edge.gemfile +7 -0
- data/gemfiles/rails42.gemfile +5 -0
- data/lib/store_attribute/active_record/store.rb +139 -0
- data/lib/store_attribute/active_record/type/typed_store.rb +95 -0
- data/lib/store_attribute/active_record.rb +1 -0
- data/lib/store_attribute/version.rb +3 -0
- data/lib/store_attribute.rb +2 -0
- data/spec/cases/store_attribute_spec.rb +148 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/store_attribute/typed_store_spec.rb +94 -0
- data/spec/support/money_type.rb +12 -0
- data/spec/support/user.rb +15 -0
- data/store_attribute.gemspec +27 -0
- metadata +153 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 36243b5e350010855d95f5104c95dead8d0f6f8a
|
4
|
+
data.tar.gz: d28d7d37b5db4a62a7ee363acdfb7b2d18e599c1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9cd64959ebac0572406300a1f3f350dc54232d8ed05639d03a9f941fdf8c0703d9ac2f23b40b589fca9b696da42e263f262231f84c613c04fd6a13c18ef68c17
|
7
|
+
data.tar.gz: 61693d8113e5f3f419e558a3f40877cdbe6ae03e1b3a7a27d5f3097d51a08540f581cd0e7f5a89ad626169fb3d4d71c6c48796df346f56e94ba2a0d88f1406e8
|
data/.gitignore
ADDED
@@ -0,0 +1,36 @@
|
|
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
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,51 @@
|
|
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
|
data/.travis.yml
ADDED
@@ -0,0 +1,14 @@
|
|
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.0
|
14
|
+
gemfile: gemfiles/rails42.gemfile
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2016 palkan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
[](https://rubygems.org/gems/store_attribute) [](https://travis-ci.org/palkan/store_attribute)
|
2
|
+
|
3
|
+
## Store Attribute
|
4
|
+
|
5
|
+
ActiveRecord extension which adds typecasting to store accessors.
|
6
|
+
|
7
|
+
Compatible with **Rails** ~> 4.2.
|
8
|
+
|
9
|
+
|
10
|
+
### Install
|
11
|
+
|
12
|
+
In your Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem "store_attribute", "~>0.4.0" # version 0.4.x is for Rails 4.2.x and 0.5.x is for Rails 5
|
16
|
+
```
|
17
|
+
|
18
|
+
### Usage
|
19
|
+
|
20
|
+
You can use `store_attribute` method to add additional accessors with a type to an existing store on a model.
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
.store_attribute(store_name, name, type, options = {})
|
24
|
+
```
|
25
|
+
|
26
|
+
Where:
|
27
|
+
- `store_name` The name of the store.
|
28
|
+
- `name` The name of the accessor to the store.
|
29
|
+
- `type` A symbol such as `:string` or `:integer`, or a type object to be used for the accessor.
|
30
|
+
- `options` A hash of cast type options such as `precision`, `limit`, `scale`.
|
31
|
+
|
32
|
+
Type casting occurs every time you write data through accessor or update store itself
|
33
|
+
and when object is loaded from database.
|
34
|
+
|
35
|
+
Note that if you update store explicitly then value isn't type casted.
|
36
|
+
|
37
|
+
Examples:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
class MegaUser < User
|
41
|
+
store_attribute :settings, :ratio, :integer, limit: 1
|
42
|
+
store_attribute :settings, :login_at, :datetime
|
43
|
+
store_attribute :settings, :active, :boolean
|
44
|
+
end
|
45
|
+
|
46
|
+
u = MegaUser.new(active: false, login_at: '2015-01-01 00:01', ratio: "63.4608")
|
47
|
+
|
48
|
+
u.login_at.is_a?(DateTime) # => true
|
49
|
+
u.login_at = DateTime.new(2015,1,1,11,0,0)
|
50
|
+
u.ratio # => 63
|
51
|
+
u.active # => false
|
52
|
+
# And we also have a predicate method
|
53
|
+
u.active? # => false
|
54
|
+
u.reload
|
55
|
+
|
56
|
+
# After loading record from db store contains casted data
|
57
|
+
u.settings['login_at'] == DateTime.new(2015,1,1,11,0,0) # => true
|
58
|
+
|
59
|
+
# If you update store explicitly then the value returned
|
60
|
+
# by accessor isn't type casted
|
61
|
+
u.settings['ration'] = "3.141592653"
|
62
|
+
u.ratio # => "3.141592653"
|
63
|
+
|
64
|
+
# On the other hand, writing through accessor set correct data within store
|
65
|
+
u.ratio = "3.14.1592653"
|
66
|
+
u.ratio # => 3
|
67
|
+
u.settings['ratio'] # => 3
|
68
|
+
```
|
69
|
+
|
70
|
+
You can also specify type using usual `store_accessor` method:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
class SuperUser < User
|
74
|
+
store_accessor :settings, :privileges, login_at: :datetime
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
Or through `store`:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
class User < ActiveRecord::Base
|
82
|
+
store :settings, accessors: [:color, :homepage, login_at: :datetime], coder: JSON
|
83
|
+
end
|
84
|
+
```
|
data/Rakefile
ADDED
data/bench/bench.rb
ADDED
@@ -0,0 +1,38 @@
|
|
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
|
data/bench/setup.rb
ADDED
@@ -0,0 +1,67 @@
|
|
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')
|
data/bin/console
ADDED
data/bin/setup
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'active_record/store'
|
2
|
+
require 'store_attribute/active_record/type/typed_store'
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
module Store
|
6
|
+
module ClassMethods # :nodoc:
|
7
|
+
# Defines store on this model.
|
8
|
+
#
|
9
|
+
# +store_name+ The name of the store.
|
10
|
+
#
|
11
|
+
# ==== Options
|
12
|
+
# The following options are accepted:
|
13
|
+
#
|
14
|
+
# +coder+ The coder of the store.
|
15
|
+
#
|
16
|
+
# +accessors+ An array of the accessors to the store.
|
17
|
+
#
|
18
|
+
# Examples:
|
19
|
+
#
|
20
|
+
# class User < ActiveRecord::Base
|
21
|
+
# store :settings, accessors: [:color, :homepage, login_at: :datetime], coder: JSON
|
22
|
+
# end
|
23
|
+
def store(store_name, options = {})
|
24
|
+
serialize store_name, IndifferentCoder.new(options[:coder])
|
25
|
+
store_accessor(store_name, *options[:accessors]) if options.key?(:accessors)
|
26
|
+
end
|
27
|
+
# Adds additional accessors to an existing store on this model.
|
28
|
+
#
|
29
|
+
# +store_name+ The name of the store.
|
30
|
+
#
|
31
|
+
# +keys+ The array of the accessors to the store.
|
32
|
+
#
|
33
|
+
# +typed_keys+ The key-to-type hash of the accesors with type to the store.
|
34
|
+
#
|
35
|
+
# Examples:
|
36
|
+
#
|
37
|
+
# class SuperUser < User
|
38
|
+
# store_accessor :settings, :privileges, login_at: :datetime
|
39
|
+
# end
|
40
|
+
def store_accessor(store_name, *keys, **typed_keys)
|
41
|
+
keys = keys.flatten
|
42
|
+
typed_keys = typed_keys.except(keys)
|
43
|
+
|
44
|
+
_define_accessors_methods(store_name, *keys)
|
45
|
+
|
46
|
+
_prepare_local_stored_attributes(store_name, *keys)
|
47
|
+
|
48
|
+
typed_keys.each do |key, type|
|
49
|
+
store_attribute(store_name, key, type)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Adds additional accessors with a type to an existing store on this model.
|
54
|
+
# Type casting occurs every time you write data through accessor or update store itself
|
55
|
+
# and when object is loaded from database.
|
56
|
+
#
|
57
|
+
# Note that if you update store explicitly then value isn't type casted.
|
58
|
+
#
|
59
|
+
# +store_name+ The name of the store.
|
60
|
+
#
|
61
|
+
# +name+ The name of the accessor to the store.
|
62
|
+
#
|
63
|
+
# +type+ A symbol such as +:string+ or +:integer+, or a type object
|
64
|
+
# to be used for the accessor.
|
65
|
+
#
|
66
|
+
# +options+ A hash of cast type options such as +precision+, +limit+, +scale+.
|
67
|
+
#
|
68
|
+
# Examples:
|
69
|
+
#
|
70
|
+
# class MegaUser < User
|
71
|
+
# store_attribute :settings, :ratio, :integer, limit: 1
|
72
|
+
# store_attribute :settings, :login_at, :datetime
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# u = MegaUser.new(active: false, login_at: '2015-01-01 00:01', ratio: "63.4608")
|
76
|
+
#
|
77
|
+
# u.login_at.is_a?(DateTime) # => true
|
78
|
+
# u.login_at = DateTime.new(2015,1,1,11,0,0)
|
79
|
+
# u.ratio # => 63
|
80
|
+
# u.reload
|
81
|
+
#
|
82
|
+
# # After loading record from db store contains casted data
|
83
|
+
# u.settings['login_at'] == DateTime.new(2015,1,1,11,0,0) # => true
|
84
|
+
#
|
85
|
+
# # If you update store explicitly then the value returned
|
86
|
+
# # by accessor isn't type casted
|
87
|
+
# u.settings['ration'] = "3.141592653"
|
88
|
+
# u.ratio # => "3.141592653"
|
89
|
+
#
|
90
|
+
# # On the other hand, writing through accessor set correct data within store
|
91
|
+
# u.ratio = "3.14.1592653"
|
92
|
+
# u.ratio # => 3
|
93
|
+
# u.settings['ratio'] # => 3
|
94
|
+
#
|
95
|
+
# For more examples on using types, see documentation for ActiveRecord::Attributes.
|
96
|
+
def store_attribute(store_name, name, type, **options)
|
97
|
+
_define_accessors_methods(store_name, name)
|
98
|
+
|
99
|
+
_define_predicate_method(name) if type == :boolean
|
100
|
+
|
101
|
+
decorate_attribute_type(store_name, "typed_accessor_for_#{name}") do |subtype|
|
102
|
+
Type::TypedStore.create_from_type(subtype, name, type, **options)
|
103
|
+
end
|
104
|
+
|
105
|
+
_prepare_local_stored_attributes(store_name, name)
|
106
|
+
end
|
107
|
+
|
108
|
+
def _prepare_local_stored_attributes(store_name, *keys) # :nodoc:
|
109
|
+
# assign new store attribute and create new hash to ensure that each class in the hierarchy
|
110
|
+
# has its own hash of stored attributes.
|
111
|
+
self.local_stored_attributes ||= {}
|
112
|
+
self.local_stored_attributes[store_name] ||= []
|
113
|
+
self.local_stored_attributes[store_name] |= keys
|
114
|
+
end
|
115
|
+
|
116
|
+
def _define_accessors_methods(store_name, *keys) # :nodoc:
|
117
|
+
_store_accessors_module.module_eval do
|
118
|
+
keys.each do |key|
|
119
|
+
define_method("#{key}=") do |value|
|
120
|
+
write_store_attribute(store_name, key, value)
|
121
|
+
end
|
122
|
+
|
123
|
+
define_method(key) do
|
124
|
+
read_store_attribute(store_name, key)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def _define_predicate_method(name)
|
131
|
+
_store_accessors_module.module_eval do
|
132
|
+
define_method("#{name}?") do
|
133
|
+
send(name) == true
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'active_record/type'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Type # :nodoc:
|
5
|
+
BASE_TYPES = {
|
6
|
+
boolean: ::ActiveRecord::Type::Boolean,
|
7
|
+
integer: ::ActiveRecord::Type::Integer,
|
8
|
+
string: ::ActiveRecord::Type::String,
|
9
|
+
float: ::ActiveRecord::Type::Float,
|
10
|
+
date: ::ActiveRecord::Type::Date,
|
11
|
+
datetime: ::ActiveRecord::Type::DateTime,
|
12
|
+
decimal: ::ActiveRecord::Type::Decimal
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def self.lookup_type(type, options)
|
16
|
+
BASE_TYPES[type.to_sym].try(:new, options) ||
|
17
|
+
ActiveRecord::Base.connection.type_map.lookup(type.to_s, options)
|
18
|
+
end
|
19
|
+
|
20
|
+
class TypedStore < DelegateClass(ActiveRecord::Type::Value) # :nodoc:
|
21
|
+
# Creates +TypedStore+ type instance and specifies type caster
|
22
|
+
# for key.
|
23
|
+
def self.create_from_type(basetype, key, type, **options)
|
24
|
+
typed_store = new(basetype)
|
25
|
+
typed_store.add_typed_key(key, type, **options)
|
26
|
+
typed_store
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(subtype)
|
30
|
+
@accessor_types = {}
|
31
|
+
@store_accessor = subtype.accessor
|
32
|
+
super(subtype)
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_typed_key(key, type, **options)
|
36
|
+
type = Type.lookup_type(type, options) if type.is_a?(Symbol)
|
37
|
+
@accessor_types[key.to_s] = type
|
38
|
+
end
|
39
|
+
|
40
|
+
def type_cast_from_database(value)
|
41
|
+
hash = super
|
42
|
+
type_cast_from_user(hash)
|
43
|
+
end
|
44
|
+
|
45
|
+
def type_cast_for_database(value)
|
46
|
+
if value
|
47
|
+
accessor_types.each do |key, type|
|
48
|
+
k = key_to_cast(value, key)
|
49
|
+
value[k] = type.type_cast_for_database(value[k]) unless k.nil?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
super(value)
|
53
|
+
end
|
54
|
+
|
55
|
+
def type_cast_from_user(value)
|
56
|
+
hash = super
|
57
|
+
if hash
|
58
|
+
accessor_types.each do |key, type|
|
59
|
+
hash[key] = type.type_cast_from_user(hash[key]) if hash.key?(key)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
hash
|
63
|
+
end
|
64
|
+
|
65
|
+
def accessor
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
def write(object, attribute, key, value)
|
70
|
+
value = type_for(key).type_cast_from_user(value) if typed?(key)
|
71
|
+
store_accessor.write(object, attribute, key, value)
|
72
|
+
end
|
73
|
+
|
74
|
+
delegate :read, :prepare, to: :store_accessor
|
75
|
+
|
76
|
+
protected
|
77
|
+
|
78
|
+
# We cannot rely on string keys 'cause user input can contain symbol keys
|
79
|
+
def key_to_cast(val, key)
|
80
|
+
return key if val.key?(key)
|
81
|
+
return key.to_sym if val.key?(key.to_sym)
|
82
|
+
end
|
83
|
+
|
84
|
+
def typed?(key)
|
85
|
+
accessor_types.key?(key.to_s)
|
86
|
+
end
|
87
|
+
|
88
|
+
def type_for(key)
|
89
|
+
accessor_types.fetch(key.to_s)
|
90
|
+
end
|
91
|
+
|
92
|
+
attr_reader :accessor_types, :store_accessor
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'store_attribute/active_record/store'
|
@@ -0,0 +1,148 @@
|
|
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
|
+
jamie = User.find(jamie.id)
|
144
|
+
|
145
|
+
expect(jamie.price).to eq 1200
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,33 @@
|
|
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
|
+
end
|
@@ -0,0 +1,94 @@
|
|
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 "#type_cast_from_user" do
|
18
|
+
it "without key types", :aggregate_failures do
|
19
|
+
expect(subject.type_cast_from_user([1, 2])).to eq [1, 2]
|
20
|
+
expect(subject.type_cast_from_user('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.type_cast_from_user(date: '2016-06-22')).to eq('date' => date)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "#type_cast_from_database" do
|
32
|
+
it "without key types", :aggregate_failures do
|
33
|
+
expect(subject.type_cast_from_database('[1,2]')).to eq [1, 2]
|
34
|
+
expect(subject.type_cast_from_database('{"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.type_cast_from_database('{"date":"2016-06-22"}')).to eq('date' => date)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#type_cast_for_database" do
|
46
|
+
it "without key types", :aggregate_failures do
|
47
|
+
expect(subject.type_cast_for_database([1, 2])).to eq '[1,2]'
|
48
|
+
expect(subject.type_cast_for_database('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.type_cast_for_database(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.type_cast_for_database(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.type_cast_from_user(date: '2016-06-22', val: '1.2')).to eq('date' => date, 'val' => '1.2')
|
73
|
+
expect(new_type.type_cast_from_user(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.type_cast_from_user(date: '2016-06-22')).to eq('date' => date)
|
87
|
+
expect(subject.type_cast_from_user('date' => '2016-06-22')).to eq('date' => date)
|
88
|
+
expect(subject.type_cast_from_database("---\n:date: 2016-06-22\n")).to eq('date' => date)
|
89
|
+
expect(subject.type_cast_from_database("---\ndate: 2016-06-22\n")).to eq('date' => date)
|
90
|
+
expect(subject.type_cast_for_database(date: date)).to eq "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\ndate: 2016-06-22\n"
|
91
|
+
expect(subject.type_cast_for_database('date' => date)).to eq "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\ndate: 2016-06-22\n"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class MoneyType < ActiveRecord::Type::Integer
|
2
|
+
def type_cast_from_user(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::Base.connection.type_map.register_type('money_type', MoneyType.new)
|
@@ -0,0 +1,15 @@
|
|
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 :custom, accessors: [price: :money_type]
|
10
|
+
|
11
|
+
store_accessor :hdata, visible: :boolean
|
12
|
+
|
13
|
+
store_attribute :hdata, :ratio, :integer, limit: 1
|
14
|
+
store_attribute :hdata, :login_at, :datetime
|
15
|
+
end
|
@@ -0,0 +1,27 @@
|
|
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", ">=4.2.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.4.0"
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: store_attribute
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- palkan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-06-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pg
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.18'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.18'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.1'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
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: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.4.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 3.4.0
|
97
|
+
description: ActiveRecord extension which adds typecasting to store accessors
|
98
|
+
email:
|
99
|
+
- dementiev.vm@gmail.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- ".rspec"
|
106
|
+
- ".rubocop.yml"
|
107
|
+
- ".travis.yml"
|
108
|
+
- Gemfile
|
109
|
+
- MIT-LICENSE
|
110
|
+
- README.md
|
111
|
+
- Rakefile
|
112
|
+
- bench/bench.rb
|
113
|
+
- bench/setup.rb
|
114
|
+
- bin/console
|
115
|
+
- bin/setup
|
116
|
+
- gemfiles/rails-edge.gemfile
|
117
|
+
- gemfiles/rails42.gemfile
|
118
|
+
- lib/store_attribute.rb
|
119
|
+
- lib/store_attribute/active_record.rb
|
120
|
+
- lib/store_attribute/active_record/store.rb
|
121
|
+
- lib/store_attribute/active_record/type/typed_store.rb
|
122
|
+
- lib/store_attribute/version.rb
|
123
|
+
- spec/cases/store_attribute_spec.rb
|
124
|
+
- spec/spec_helper.rb
|
125
|
+
- spec/store_attribute/typed_store_spec.rb
|
126
|
+
- spec/support/money_type.rb
|
127
|
+
- spec/support/user.rb
|
128
|
+
- store_attribute.gemspec
|
129
|
+
homepage: http://github.com/palkan/store_attribute
|
130
|
+
licenses:
|
131
|
+
- MIT
|
132
|
+
metadata: {}
|
133
|
+
post_install_message:
|
134
|
+
rdoc_options: []
|
135
|
+
require_paths:
|
136
|
+
- lib
|
137
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
requirements: []
|
148
|
+
rubyforge_project:
|
149
|
+
rubygems_version: 2.6.4
|
150
|
+
signing_key:
|
151
|
+
specification_version: 4
|
152
|
+
summary: ActiveRecord extension which adds typecasting to store accessors
|
153
|
+
test_files: []
|