activerecord-typedstore 0.3.2 → 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 +4 -4
- data/.travis.yml +5 -0
- data/README.md +14 -5
- data/gemfiles/Gemfile.ar-4.1 +12 -0
- data/lib/active_record/typed_store/ar_32_fallbacks.rb +25 -4
- data/lib/active_record/typed_store/coder.rb +22 -0
- data/lib/active_record/typed_store/column.rb +5 -0
- data/lib/active_record/typed_store/dsl.rb +5 -4
- data/lib/active_record/typed_store/extension.rb +21 -60
- data/lib/active_record/typed_store/typed_hash.rb +63 -0
- data/lib/active_record/typed_store/version.rb +1 -1
- data/spec/active_record/typed_store/typed_hash_spec.rb +204 -0
- data/spec/active_record/typed_store_spec.rb +21 -33
- data/spec/support/models.rb +15 -1
- metadata +7 -3
- data/lib/active_record/typed_store/ar_41_fallbacks.rb +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 960e7c0f770dab59201ece58bcfa91927216eeea
|
4
|
+
data.tar.gz: 5a9f38f6153167050d7e5a57bfcba6fb717dd9e6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 363573dd39996321d853554cc49c1712966a515e8260bfaa9e6b6bf2763b8d5600b516e76559eff1e480c4527f43f472e403573b9a44b5b986307550345a3455
|
7
|
+
data.tar.gz: 2b7945d7a87685f0178cca67c84eb662feb21cafe2d944a4883265917508dacf70f75bb1cffef473338dd870c5f44c3069f2f16f2cc4cf3c34c0e66ce077707f
|
data/.travis.yml
CHANGED
@@ -4,6 +4,7 @@ rvm:
|
|
4
4
|
gemfile:
|
5
5
|
- gemfiles/Gemfile.ar-3.2
|
6
6
|
- gemfiles/Gemfile.ar-4.0
|
7
|
+
- gemfiles/Gemfile.ar-4.1
|
7
8
|
- gemfiles/Gemfile.ar-edge
|
8
9
|
|
9
10
|
before_script:
|
@@ -12,3 +13,7 @@ before_script:
|
|
12
13
|
env:
|
13
14
|
- TIMEZONE_AWARE=1 POSTGRES=1 MYSQL=1 POSTGRES_URL=$(curl http://api.postgression.com)
|
14
15
|
- TIMEZONE_AWARE=0 POSTGRES=1 MYSQL=1 POSTGRES_URL=$(curl http://api.postgression.com)
|
16
|
+
|
17
|
+
matrix:
|
18
|
+
allow_failures:
|
19
|
+
- gemfile: gemfiles/Gemfile.ar-edge
|
data/README.md
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
[](http://travis-ci.org/byroot/activerecord-typedstore)
|
4
4
|
[](https://codeclimate.com/github/byroot/activerecord-typedstore)
|
5
5
|
[](https://coveralls.io/r/byroot/activerecord-typedstore)
|
6
|
+
[](http://badge.fury.io/rb/activerecord-typedstore)
|
6
7
|
|
7
8
|
[ActiveRecord::Store](http://api.rubyonrails.org/classes/ActiveRecord/Store.html) but with typed attributes.
|
8
9
|
|
@@ -78,6 +79,19 @@ shop.save
|
|
78
79
|
shop.reload
|
79
80
|
shop.settings[:unknown] # => 'Hello World'
|
80
81
|
|
82
|
+
# If you only want type casting and default handling without accessors
|
83
|
+
|
84
|
+
# you can disable them store wide
|
85
|
+
typed_store :settings, accessors: false do |s|
|
86
|
+
# ...
|
87
|
+
end
|
88
|
+
|
89
|
+
# or on a per attribute basis
|
90
|
+
typed_store :settings do |s|
|
91
|
+
s.integer :age
|
92
|
+
s.string :postal_code, accessor: false
|
93
|
+
end
|
94
|
+
|
81
95
|
```
|
82
96
|
|
83
97
|
Type casting rules and attribute behavior are exactly the same as a for real database columns.
|
@@ -92,8 +106,3 @@ If not, please fill an issue.
|
|
92
106
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
93
107
|
4. Push to the branch (`git push origin my-new-feature`)
|
94
108
|
5. Create new Pull Request
|
95
|
-
|
96
|
-
## TODO
|
97
|
-
|
98
|
-
- HStore support with ActiveRecord 4.1.0.beta (master)
|
99
|
-
- Handle casting and default at the store layer, so accessors are not mandatory anymore. See #4
|
@@ -1,3 +1,24 @@
|
|
1
|
+
class ActiveRecord::Store::IndifferentCoder
|
2
|
+
# Backport from rails 4.0
|
3
|
+
def initialize(coder_or_class_name)
|
4
|
+
@coder =
|
5
|
+
if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump)
|
6
|
+
coder_or_class_name
|
7
|
+
else
|
8
|
+
ActiveRecord::Coders::YAMLColumn.new(coder_or_class_name || Object)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def dump(obj)
|
13
|
+
@coder.dump self.class.as_indifferent_hash(obj)
|
14
|
+
end
|
15
|
+
|
16
|
+
def load(yaml)
|
17
|
+
self.class.as_indifferent_hash @coder.load(yaml)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
1
22
|
module ActiveRecord::TypedStore
|
2
23
|
|
3
24
|
module AR32Fallbacks
|
@@ -11,7 +32,7 @@ module ActiveRecord::TypedStore
|
|
11
32
|
module ClassMethods
|
12
33
|
|
13
34
|
def typed_store(store_attribute, dsl)
|
14
|
-
_ar_32_fallback_accessors(store_attribute, dsl.
|
35
|
+
_ar_32_fallback_accessors(store_attribute, dsl.accessors)
|
15
36
|
end
|
16
37
|
|
17
38
|
protected
|
@@ -34,9 +55,9 @@ module ActiveRecord::TypedStore
|
|
34
55
|
attribute_method_matchers_cache.clear
|
35
56
|
end
|
36
57
|
|
37
|
-
def _ar_32_fallback_accessors(store_attribute,
|
38
|
-
|
39
|
-
_ar_32_fallback_accessor(store_attribute,
|
58
|
+
def _ar_32_fallback_accessors(store_attribute, accessors)
|
59
|
+
accessors.each do |accessor|
|
60
|
+
_ar_32_fallback_accessor(store_attribute, accessor)
|
40
61
|
end
|
41
62
|
end
|
42
63
|
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ActiveRecord::TypedStore
|
2
|
+
|
3
|
+
class Coder < ::ActiveRecord::Store::IndifferentCoder
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def create(store_class)
|
8
|
+
Class.new(self) do
|
9
|
+
@store_class = store_class
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def as_indifferent_hash(obj)
|
14
|
+
return obj if obj.is_a?(@store_class)
|
15
|
+
@store_class.new(obj)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -10,6 +10,11 @@ module ActiveRecord::TypedStore
|
|
10
10
|
@default = extract_default(options.fetch(:default, nil))
|
11
11
|
@null = options.fetch(:null, true)
|
12
12
|
@blank = options.fetch(:blank, true)
|
13
|
+
@accessor = options.fetch(:accessor, true)
|
14
|
+
end
|
15
|
+
|
16
|
+
def accessor?
|
17
|
+
@accessor
|
13
18
|
end
|
14
19
|
|
15
20
|
def cast(value)
|
@@ -4,18 +4,19 @@ module ActiveRecord::TypedStore
|
|
4
4
|
|
5
5
|
attr_reader :columns
|
6
6
|
|
7
|
-
def initialize
|
7
|
+
def initialize(accessors=true)
|
8
|
+
@accessors = accessors
|
8
9
|
@columns = []
|
9
10
|
yield self
|
10
11
|
end
|
11
12
|
|
12
|
-
def
|
13
|
-
@columns.map(&:name)
|
13
|
+
def accessors
|
14
|
+
@columns.select(&:accessor?).map(&:name)
|
14
15
|
end
|
15
16
|
|
16
17
|
[:string, :integer, :float, :decimal, :datetime, :date, :boolean, :any].each do |type|
|
17
18
|
define_method(type) do |name, options={}|
|
18
|
-
@columns << Column.new(name, type, options)
|
19
|
+
@columns << Column.new(name, type, options.reverse_merge(accessor: @accessors))
|
19
20
|
end
|
20
21
|
end
|
21
22
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'active_record/typed_store/column'
|
2
2
|
require 'active_record/typed_store/dsl'
|
3
|
+
require 'active_record/typed_store/typed_hash'
|
3
4
|
|
4
5
|
module ActiveRecord::TypedStore
|
5
6
|
AR_VERSION = Gem::Version.new(ActiveRecord::VERSION::STRING)
|
@@ -17,18 +18,15 @@ module ActiveRecord::TypedStore
|
|
17
18
|
module ClassMethods
|
18
19
|
|
19
20
|
def typed_store(store_attribute, options={}, &block)
|
20
|
-
dsl = DSL.new(&block)
|
21
|
+
dsl = DSL.new(options.fetch(:accessors, true), &block)
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
else
|
25
|
-
store(store_attribute, options.merge(accessors: dsl.column_names))
|
26
|
-
end
|
23
|
+
serialize store_attribute, create_coder(store_attribute, dsl.columns).new(options[:coder])
|
24
|
+
store_accessor(store_attribute, dsl.accessors)
|
27
25
|
|
28
26
|
register_typed_store_columns(store_attribute, dsl.columns)
|
29
27
|
super(store_attribute, dsl) if defined?(super)
|
30
28
|
|
31
|
-
dsl.
|
29
|
+
dsl.accessors.each { |c| define_store_attribute_queries(store_attribute, c) }
|
32
30
|
|
33
31
|
dsl
|
34
32
|
end
|
@@ -40,6 +38,12 @@ module ActiveRecord::TypedStore
|
|
40
38
|
|
41
39
|
private
|
42
40
|
|
41
|
+
def create_coder(store_attribute, columns)
|
42
|
+
store_class = TypedHash.create(columns)
|
43
|
+
const_set("#{store_attribute}_hash".camelize, store_class)
|
44
|
+
Coder.create(store_class)
|
45
|
+
end
|
46
|
+
|
43
47
|
def register_typed_store_columns(store_attribute, columns)
|
44
48
|
self.typed_stores ||= {}
|
45
49
|
self.typed_store_attributes ||= {}
|
@@ -49,12 +53,16 @@ module ActiveRecord::TypedStore
|
|
49
53
|
end
|
50
54
|
|
51
55
|
def define_typed_store_attribute_methods
|
52
|
-
|
53
|
-
typed_store_attributes.keys.each do |attribute|
|
56
|
+
store_accessors.each do |attribute|
|
54
57
|
define_virtual_attribute_method(attribute)
|
55
58
|
end
|
56
59
|
end
|
57
60
|
|
61
|
+
def store_accessors
|
62
|
+
return [] unless typed_store_attributes
|
63
|
+
typed_store_attributes.values.select(&:accessor?).map(&:name).map(&:to_s)
|
64
|
+
end
|
65
|
+
|
58
66
|
def hstore?(store_attribute)
|
59
67
|
columns_hash[store_attribute.to_s].try(:type) == :hstore
|
60
68
|
end
|
@@ -72,11 +80,6 @@ module ActiveRecord::TypedStore
|
|
72
80
|
|
73
81
|
end
|
74
82
|
|
75
|
-
def reload(*)
|
76
|
-
reload_stores!
|
77
|
-
super
|
78
|
-
end
|
79
|
-
|
80
83
|
protected
|
81
84
|
|
82
85
|
def write_store_attribute(store_attribute, key, value)
|
@@ -86,18 +89,12 @@ module ActiveRecord::TypedStore
|
|
86
89
|
end
|
87
90
|
|
88
91
|
previous_value = read_store_attribute(store_attribute, key)
|
89
|
-
|
90
|
-
|
91
|
-
super(store_attribute, key, casted_value)
|
92
|
+
attribute_will_change!(key.to_s) if value != previous_value
|
93
|
+
super
|
92
94
|
end
|
93
95
|
|
94
96
|
private
|
95
97
|
|
96
|
-
def cast_store_attribute(store_attribute, key, value)
|
97
|
-
column = store_column(store_attribute, key)
|
98
|
-
column ? column.cast(value) : value
|
99
|
-
end
|
100
|
-
|
101
98
|
def store_column(store_attribute, key)
|
102
99
|
store = store_columns(store_attribute)
|
103
100
|
store && store[key]
|
@@ -107,43 +104,6 @@ module ActiveRecord::TypedStore
|
|
107
104
|
self.class.typed_stores.try(:[], store_attribute)
|
108
105
|
end
|
109
106
|
|
110
|
-
def if_store_uninitialized(store_attribute)
|
111
|
-
initialized = "@_#{store_attribute}_initialized"
|
112
|
-
unless instance_variable_get(initialized)
|
113
|
-
yield
|
114
|
-
instance_variable_set(initialized, true)
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
def reload_stores!
|
119
|
-
return unless self.class.typed_stores
|
120
|
-
self.class.typed_stores.keys.each do |store_attribute|
|
121
|
-
instance_variable_set("@_#{store_attribute}_initialized", false)
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
def initialize_store_attribute(store_attribute)
|
126
|
-
store = defined?(super) ? super : send(store_attribute)
|
127
|
-
store.tap do |store|
|
128
|
-
if_store_uninitialized(store_attribute) do
|
129
|
-
if columns = store_columns(store_attribute)
|
130
|
-
initialize_store(store, columns.values)
|
131
|
-
end
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
def initialize_store(store, columns)
|
137
|
-
columns.each do |column|
|
138
|
-
if store.has_key?(column.name)
|
139
|
-
store[column.name] = column.cast(store[column.name])
|
140
|
-
else
|
141
|
-
store[column.name] = column.default if column.has_default?
|
142
|
-
end
|
143
|
-
end
|
144
|
-
store
|
145
|
-
end
|
146
|
-
|
147
107
|
# heavilly inspired from ActiveRecord::Base#query_attribute
|
148
108
|
def query_store_attribute(store_attribute, key)
|
149
109
|
value = read_store_attribute(store_attribute, key)
|
@@ -165,7 +125,8 @@ module ActiveRecord::TypedStore
|
|
165
125
|
end
|
166
126
|
|
167
127
|
require 'active_record/typed_store/ar_32_fallbacks' if IS_AR_3_2
|
168
|
-
require 'active_record/typed_store/
|
128
|
+
require 'active_record/typed_store/coder'
|
129
|
+
|
169
130
|
unless IS_AR_3_2
|
170
131
|
ActiveModel::AttributeMethods::ClassMethods.send(:alias_method, :define_virtual_attribute_method, :define_attribute_method)
|
171
132
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module ActiveRecord::TypedStore
|
2
|
+
|
3
|
+
class TypedHash < HashWithIndifferentAccess
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
attr_reader :columns
|
8
|
+
|
9
|
+
def create(columns)
|
10
|
+
Class.new(self) do
|
11
|
+
@columns = columns.index_by { |c| c.name.to_s }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(constructor={})
|
18
|
+
super()
|
19
|
+
update(defaults_hash)
|
20
|
+
update(constructor) if constructor.is_a?(Hash)
|
21
|
+
end
|
22
|
+
|
23
|
+
def []=(key, value)
|
24
|
+
super(key, cast_value(key, value))
|
25
|
+
end
|
26
|
+
alias_method :store, :[]=
|
27
|
+
|
28
|
+
def merge!(other_hash)
|
29
|
+
other_hash.each_pair do |key, value|
|
30
|
+
if block_given? && key?(key)
|
31
|
+
value = yield(convert_key(key), self[key], value)
|
32
|
+
end
|
33
|
+
self[convert_key(key)] = convert_value(value)
|
34
|
+
end
|
35
|
+
self
|
36
|
+
end
|
37
|
+
alias_method :update, :merge!
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
delegate :columns, to: 'self.class'
|
42
|
+
|
43
|
+
def defaults_hash
|
44
|
+
Hash[self.class.columns.values.select(&:has_default?).map { |c| [c.name, c.default] }]
|
45
|
+
end
|
46
|
+
|
47
|
+
def cast_value(key, value)
|
48
|
+
key = convert_key(key)
|
49
|
+
column = columns[key]
|
50
|
+
return value unless columns
|
51
|
+
|
52
|
+
casted_value = column.cast(value)
|
53
|
+
|
54
|
+
if casted_value.nil? && !column.null && column.has_default?
|
55
|
+
return column.default
|
56
|
+
end
|
57
|
+
|
58
|
+
casted_value
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ActiveRecord::TypedStore::TypedHash do
|
4
|
+
|
5
|
+
def create_hash_class(*args)
|
6
|
+
described_class.create([ActiveRecord::TypedStore::Column.new(*args)])
|
7
|
+
end
|
8
|
+
|
9
|
+
def build_hash(*args)
|
10
|
+
create_hash_class(*args).new
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:hash) { build_hash(*column) }
|
14
|
+
|
15
|
+
let(:hash_class) { create_hash_class(*column) }
|
16
|
+
|
17
|
+
context 'nullable column without default' do
|
18
|
+
|
19
|
+
let(:column) { ['age', :integer] }
|
20
|
+
|
21
|
+
describe '.new' do
|
22
|
+
|
23
|
+
it 'apply casting' do
|
24
|
+
hash = hash_class.new(age: '24')
|
25
|
+
expect(hash[:age]).to be == 24
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#initialize' do
|
31
|
+
|
32
|
+
it 'has nil as default value' do
|
33
|
+
expect(hash[:age]).to be_nil
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#[]=' do
|
39
|
+
|
40
|
+
it 'apply casting' do
|
41
|
+
hash[:age] = '24'
|
42
|
+
expect(hash[:age]).to be == 24
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'can be nil' do
|
46
|
+
hash[:age] = nil
|
47
|
+
expect(hash[:age]).to be_nil
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#merge!' do
|
53
|
+
|
54
|
+
it 'apply casting' do
|
55
|
+
hash.merge!(age: '24')
|
56
|
+
expect(hash[:age]).to be == 24
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'can be nil' do
|
60
|
+
hash.merge!(age: nil)
|
61
|
+
expect(hash[:age]).to be_nil
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'nullable column with default' do
|
69
|
+
|
70
|
+
let(:column) { ['age', :integer, default: 42] }
|
71
|
+
|
72
|
+
describe '#initialize' do
|
73
|
+
|
74
|
+
it 'has the default value' do
|
75
|
+
expect(hash[:age]).to be == 42
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
describe '#[]=' do
|
81
|
+
|
82
|
+
it 'apply casting' do
|
83
|
+
hash[:age] = '24'
|
84
|
+
expect(hash[:age]).to be == 24
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'can be nil' do
|
88
|
+
hash[:age] = nil
|
89
|
+
expect(hash[:age]).to be_nil
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '#merge!' do
|
95
|
+
|
96
|
+
it 'apply casting' do
|
97
|
+
hash.merge!(age: '24')
|
98
|
+
expect(hash[:age]).to be == 24
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'can be nil' do
|
102
|
+
hash.merge!(age: nil)
|
103
|
+
expect(hash[:age]).to be_nil
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'non nullable column with default' do
|
111
|
+
|
112
|
+
let(:column) { ['age', :integer, null: false, default: 42] }
|
113
|
+
|
114
|
+
describe '#intialize' do
|
115
|
+
|
116
|
+
it 'has the default value' do
|
117
|
+
expect(hash[:age]).to be == 42
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
describe '#[]=' do
|
123
|
+
|
124
|
+
it 'apply casting' do
|
125
|
+
hash[:age] = '24'
|
126
|
+
expect(hash[:age]).to be == 24
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'cannot be nil' do
|
130
|
+
hash[:age] = nil
|
131
|
+
expect(hash[:age]).to be == 42
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
describe '#merge!' do
|
137
|
+
|
138
|
+
it 'apply casting' do
|
139
|
+
hash.merge!(age: '24')
|
140
|
+
expect(hash[:age]).to be == 24
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'cannot be nil' do
|
144
|
+
hash.merge!(age: nil)
|
145
|
+
expect(hash[:age]).to be == 42
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'non blankable column with default' do
|
153
|
+
|
154
|
+
let(:column) { ['source', :string, blank: false, default: 'web'] }
|
155
|
+
|
156
|
+
describe '#intialize' do
|
157
|
+
|
158
|
+
it 'has the default value' do
|
159
|
+
expect(hash[:source]).to be == 'web'
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
describe '#[]=' do
|
165
|
+
|
166
|
+
it 'apply casting' do
|
167
|
+
hash[:source] = :mailing
|
168
|
+
expect(hash[:source]).to be == 'mailing'
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'cannot be nil' do
|
172
|
+
hash[:source] = nil
|
173
|
+
expect(hash[:source]).to be == 'web'
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'cannot be blank' do
|
177
|
+
hash[:source] = ''
|
178
|
+
expect(hash[:source]).to be == 'web'
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
describe '#merge!' do
|
184
|
+
|
185
|
+
it 'apply casting' do
|
186
|
+
hash.merge!(source: :mailing)
|
187
|
+
expect(hash[:source]).to be == 'mailing'
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'cannot be nil' do
|
191
|
+
hash.merge!(source: nil)
|
192
|
+
expect(hash[:source]).to be == 'web'
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'cannot be blank' do
|
196
|
+
hash.merge!(source: '')
|
197
|
+
expect(hash[:source]).to be == 'web'
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
@@ -447,39 +447,6 @@ shared_examples 'a store' do |retain_type=true|
|
|
447
447
|
|
448
448
|
let(:model) { described_class.new }
|
449
449
|
|
450
|
-
describe 'initialization' do
|
451
|
-
|
452
|
-
it 'is done only once' do
|
453
|
-
model.should_receive(:initialize_store).once
|
454
|
-
3.times do
|
455
|
-
model.age = (rand * 100).to_i
|
456
|
-
model.age
|
457
|
-
end
|
458
|
-
end
|
459
|
-
|
460
|
-
it 'is done again after a reload' do
|
461
|
-
model.save
|
462
|
-
|
463
|
-
model.should_receive(:initialize_store).twice
|
464
|
-
3.times do
|
465
|
-
model.age = (rand * 100).to_i
|
466
|
-
model.age
|
467
|
-
end
|
468
|
-
model.reload
|
469
|
-
3.times do
|
470
|
-
model.age = (rand * 100).to_i
|
471
|
-
model.age
|
472
|
-
end
|
473
|
-
end
|
474
|
-
|
475
|
-
it 'is not performe if no store attributes are accessed' do
|
476
|
-
model.should_not_receive(:initialize_store)
|
477
|
-
model.update_attributes(untyped_settings: {foo: :bar})
|
478
|
-
model.update_attributes(untyped_settings: {})
|
479
|
-
end
|
480
|
-
|
481
|
-
end
|
482
|
-
|
483
450
|
describe 'attributes' do
|
484
451
|
|
485
452
|
it 'retrieve default if assigned nil and null not allowed' do
|
@@ -494,6 +461,27 @@ shared_examples 'a store' do |retain_type=true|
|
|
494
461
|
|
495
462
|
end
|
496
463
|
|
464
|
+
describe 'attributes without accessors' do
|
465
|
+
|
466
|
+
it 'cannot be accessed as a model attribute' do
|
467
|
+
expect(model).to_not respond_to :country
|
468
|
+
expect(model).to_not respond_to :country=
|
469
|
+
end
|
470
|
+
|
471
|
+
it 'cannot be queried' do
|
472
|
+
expect(model).to_not respond_to :country?
|
473
|
+
end
|
474
|
+
|
475
|
+
it 'cannot be reset' do
|
476
|
+
expect(model).to_not respond_to :reset_country!
|
477
|
+
end
|
478
|
+
|
479
|
+
it 'still has casting a default handling' do
|
480
|
+
expect(model.settings[:country]).to be == 'Canada'
|
481
|
+
end
|
482
|
+
|
483
|
+
end
|
484
|
+
|
497
485
|
describe '`any` attributes' do
|
498
486
|
|
499
487
|
it 'accept any type' do
|
data/spec/support/models.rb
CHANGED
@@ -48,6 +48,7 @@ def define_store_columns(t)
|
|
48
48
|
define_columns(t)
|
49
49
|
t.any :author
|
50
50
|
t.any :source, blank: false, default: 'web'
|
51
|
+
t.string :country, blank: false, default: 'Canada', accessor: false
|
51
52
|
end
|
52
53
|
|
53
54
|
class CreateAllTables < ActiveRecord::Migration
|
@@ -171,10 +172,23 @@ class JsonTypedStoreModel < ActiveRecord::Base
|
|
171
172
|
end
|
172
173
|
end
|
173
174
|
|
175
|
+
module MarshalCoder
|
176
|
+
extend self
|
177
|
+
|
178
|
+
def load(serial)
|
179
|
+
Marshal.load(Base64.decode64(serial))
|
180
|
+
end
|
181
|
+
|
182
|
+
def dump(value)
|
183
|
+
Base64.encode64(Marshal.dump(value))
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
|
174
188
|
class MarshalTypedStoreModel < ActiveRecord::Base
|
175
189
|
establish_connection 'test_sqlite3'
|
176
190
|
store :untyped_settings, accessors: [:title]
|
177
|
-
typed_store :settings, coder: ColumnCoder.new(
|
191
|
+
typed_store :settings, coder: ColumnCoder.new(MarshalCoder) do |s|
|
178
192
|
define_store_columns(s)
|
179
193
|
end
|
180
194
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-typedstore
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Boussier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-12-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -153,15 +153,18 @@ files:
|
|
153
153
|
- activerecord-typedstore.gemspec
|
154
154
|
- gemfiles/Gemfile.ar-3.2
|
155
155
|
- gemfiles/Gemfile.ar-4.0
|
156
|
+
- gemfiles/Gemfile.ar-4.1
|
156
157
|
- gemfiles/Gemfile.ar-edge
|
157
158
|
- lib/active_record/typed_store.rb
|
158
159
|
- lib/active_record/typed_store/ar_32_fallbacks.rb
|
159
|
-
- lib/active_record/typed_store/
|
160
|
+
- lib/active_record/typed_store/coder.rb
|
160
161
|
- lib/active_record/typed_store/column.rb
|
161
162
|
- lib/active_record/typed_store/dsl.rb
|
162
163
|
- lib/active_record/typed_store/extension.rb
|
164
|
+
- lib/active_record/typed_store/typed_hash.rb
|
163
165
|
- lib/active_record/typed_store/version.rb
|
164
166
|
- lib/activerecord-typedstore.rb
|
167
|
+
- spec/active_record/typed_store/typed_hash_spec.rb
|
165
168
|
- spec/active_record/typed_store_spec.rb
|
166
169
|
- spec/spec_helper.rb
|
167
170
|
- spec/support/database_cleaner.rb
|
@@ -191,6 +194,7 @@ signing_key:
|
|
191
194
|
specification_version: 4
|
192
195
|
summary: ActiveRecord::Store but with type definition
|
193
196
|
test_files:
|
197
|
+
- spec/active_record/typed_store/typed_hash_spec.rb
|
194
198
|
- spec/active_record/typed_store_spec.rb
|
195
199
|
- spec/spec_helper.rb
|
196
200
|
- spec/support/database_cleaner.rb
|
@@ -1,26 +0,0 @@
|
|
1
|
-
module ActiveRecord::TypedStore
|
2
|
-
|
3
|
-
module AR41Fallbacks
|
4
|
-
|
5
|
-
private
|
6
|
-
|
7
|
-
module HashAccessorPatch
|
8
|
-
|
9
|
-
def self.extended(hash_accessor)
|
10
|
-
hash_accessor.singleton_class.alias_method_chain :prepare, :initialization
|
11
|
-
end
|
12
|
-
|
13
|
-
protected
|
14
|
-
|
15
|
-
def prepare_with_initialization(object, store_attribute)
|
16
|
-
prepare_without_initialization(object, store_attribute)
|
17
|
-
object.send(:initialize_store_attribute, store_attribute)
|
18
|
-
end
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
ActiveRecord::Store::HashAccessor.extend(HashAccessorPatch)
|
23
|
-
ActiveRecord::Store::IndifferentHashAccessor.extend(HashAccessorPatch)
|
24
|
-
end
|
25
|
-
|
26
|
-
end
|