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 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
@@ -0,0 +1,11 @@
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', '~>4.2'
11
+ end
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
+ [![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
+ ## 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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rspec/core/rake_task'
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "store_attribute"
5
+
6
+ require "pry"
7
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/bin/sh
2
+
3
+ set -e
4
+
5
+ gem install bundler --conservative
6
+ bundle check || bundle install
7
+
8
+ createdb store_attribute_test
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rack', github: "rack/rack"
4
+ gem 'arel', github: 'rails/arel'
5
+ gem 'rails', github: 'rails/rails'
6
+
7
+ gemspec path: '..'
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', "~>4.2"
4
+
5
+ gemspec path: '..'
@@ -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,3 @@
1
+ module StoreAttribute # :nodoc:
2
+ VERSION = "0.4.0".freeze
3
+ end
@@ -0,0 +1,2 @@
1
+ require 'store_attribute/version'
2
+ require 'store_attribute/active_record'
@@ -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
@@ -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: []