store_attribute 0.4.0

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