activerecord-typedstore 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 542197f677322bd49dc623661de6f72adae6e259
4
- data.tar.gz: 8612b393a7cbc50203a8848c9928dfade4776ef2
3
+ metadata.gz: 4c3d4b1b4f103d05dc3371d1e8ebeb7f666ac227
4
+ data.tar.gz: 33f47581329169894a7c7c9f3b79cec592b62eb4
5
5
  SHA512:
6
- metadata.gz: a06189e792adec4ac11a2bc6020439135051a1d7194f21d43008d9077fcef511ddcf9f8d252f5f25478aa9220425b38bc03655462a6138b04e9e941bcfdcc566
7
- data.tar.gz: dfc9469b4dd7c9e1dbb611db6db1e150ac643d6f52f890f58665308f384457cf5a0b2f6a7e885af51ff7db48f16d996971a3b12229bf8e226bd53a9c188c0bcb
6
+ metadata.gz: 6e35186b4dfbc9b54984d4e5a350035ecc8c64136f827ab3762586aeb14477699d2035fcce317d113402c393c068103acdddd23415ec6d69f879040d1831c111
7
+ data.tar.gz: b32badf3a634ba1aa2b572129622e9a4a2650bfefef3fbf470ee37f891c0080225d1fdb054aa0089b299dd6d0e051c26b62cb02e061e415e5b9fb3877b6ac228
data/Rakefile CHANGED
@@ -1,7 +1,6 @@
1
- require "bundler/gem_tasks"
1
+ require 'bundler/gem_tasks'
2
2
  require 'rspec/core/rake_task'
3
3
 
4
- Bundler::GemHelper.install_tasks
5
4
  RSpec::Core::RakeTask.new(:spec)
6
5
 
7
6
  task :default => :spec
@@ -10,9 +10,8 @@ module ActiveRecord::TypedStore
10
10
 
11
11
  module ClassMethods
12
12
 
13
- def typed_store(store_attribute, options={}, &block)
14
- dsl = super
15
- _ar_32_fallback_accessors(store_attribute, dsl.columns)
13
+ def typed_store(store_attribute, dsl)
14
+ _ar_32_fallback_accessors(store_attribute, dsl.column_names)
16
15
  end
17
16
 
18
17
  protected
@@ -35,65 +34,41 @@ module ActiveRecord::TypedStore
35
34
  attribute_method_matchers_cache.clear
36
35
  end
37
36
 
38
- def _ar_32_fallback_accessors(store_attribute, columns)
39
- _ar_32_fallback_initializer(store_attribute, columns)
40
- columns.each do |column|
41
- _ar_32_fallback_accessor(store_attribute, column)
37
+ def _ar_32_fallback_accessors(store_attribute, column_names)
38
+ column_names.each do |name|
39
+ _ar_32_fallback_accessor(store_attribute, name)
42
40
  end
43
41
  end
44
42
 
45
- def _ar_32_fallback_initializer(store_attribute, columns)
46
- define_method(store_attribute) do
47
- initialized = "@_#{store_attribute}_initialized"
48
- unless instance_variable_get(initialized)
49
- store = read_attribute(store_attribute)
50
- store = initialize_store(store, columns)
51
- write_attribute(store_attribute, store)
52
- instance_variable_set(initialized, true)
53
- end
54
- read_attribute(store_attribute)
43
+ def _ar_32_fallback_accessor(store_attribute, key)
44
+ define_method("#{key}=") do |value|
45
+ write_store_attribute(store_attribute, key, value)
55
46
  end
56
- end
57
-
58
- def _ar_32_fallback_accessor(store_attribute, column)
59
- _ar_32_fallback_writer(store_attribute, column)
60
- _ar_32_fallback_reader(store_attribute, column)
61
- end
62
-
63
- def _ar_32_fallback_writer(store_attribute, column)
64
- define_method("#{column.name}_with_type_casting=") do |value|
65
- casted_value = column.cast(value)
66
- attribute_will_change!(column.name.to_s) if casted_value != send(column.name)
67
- send("#{column.name}_without_type_casting=", casted_value)
47
+ define_method(key) do
48
+ read_store_attribute(store_attribute, key)
68
49
  end
69
- alias_method_chain "#{column.name}=", :type_casting
70
50
  end
71
51
 
72
- def _ar_32_fallback_reader(store_attribute, column)
73
- define_method(column.name) do
74
- send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash)
75
- store = send(store_attribute)
52
+ end
76
53
 
77
- store.has_key?(column.name) ? store[column.name] : column.default
78
- end
79
- end
54
+ private
80
55
 
56
+ def initialize_store_attribute(store_attribute)
57
+ send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash)
58
+ send(store_attribute)
81
59
  end
82
60
 
83
- def reload(*)
84
- _ar_32_reload_stores!
85
- super
61
+ def read_store_attribute(store_attribute, key)
62
+ store = initialize_store_attribute(store_attribute)
63
+ store[key]
86
64
  end
87
65
 
88
- private
89
-
90
- def _ar_32_reload_stores!
91
- self.class.stored_typed_attributes.keys.each do |store_attribute|
92
- instance_variable_set("@_#{store_attribute}_initialized", false)
93
- end
66
+ def write_store_attribute(store_attribute, key, value)
67
+ attribute_will_change!(store_attribute.to_s)
68
+ send(store_attribute)[key] = value
94
69
  end
95
70
 
96
71
  end
97
72
 
98
- ActiveRecord::Base.send(:include, AR32Fallbacks)
73
+ Extension.send(:include, AR32Fallbacks)
99
74
  end
@@ -2,19 +2,8 @@ module ActiveRecord::TypedStore
2
2
 
3
3
  module AR41Fallbacks
4
4
 
5
- def reload(*)
6
- _ar_41_reload_stores!
7
- super
8
- end
9
-
10
5
  private
11
6
 
12
- def _ar_41_reload_stores!
13
- self.class.stored_typed_attributes.keys.each do |store_attribute|
14
- instance_variable_set("@_#{store_attribute}_initialized", false)
15
- end
16
- end
17
-
18
7
  module HashAccessorPatch
19
8
 
20
9
  def self.extended(hash_accessor)
@@ -23,16 +12,9 @@ module ActiveRecord::TypedStore
23
12
 
24
13
  protected
25
14
 
26
- def prepare_with_initialization(object, attribute)
27
- prepare_without_initialization(object, attribute)
28
-
29
- initialized = "@_#{attribute}_initialized"
30
- unless object.instance_variable_get(initialized)
31
- store = object.send(:initialize_store_attribute, attribute)
32
- object.send("#{attribute}=", store)
33
- object.instance_variable_set(initialized, true)
34
- end
35
-
15
+ def prepare_with_initialization(object, store_attribute)
16
+ prepare_without_initialization(object, store_attribute)
17
+ object.send(:initialize_store_attribute, store_attribute)
36
18
  end
37
19
 
38
20
  end
@@ -41,5 +23,4 @@ module ActiveRecord::TypedStore
41
23
  ActiveRecord::Store::IndifferentHashAccessor.extend(HashAccessorPatch)
42
24
  end
43
25
 
44
- ActiveRecord::Base.send(:include, AR41Fallbacks)
45
26
  end
@@ -22,6 +22,11 @@ module ActiveRecord::TypedStore
22
22
  casted_value
23
23
  end
24
24
 
25
+ def extract_default(value)
26
+ return value if (type == :string || type == :text) && value.nil?
27
+ super
28
+ end
29
+
25
30
  def type_cast(value, map=true)
26
31
  if array && (map || value.is_a?(Array))
27
32
  return [] if map && !value.is_a?(Array)
@@ -4,21 +4,14 @@ require 'active_record/typed_store/dsl'
4
4
  module ActiveRecord::TypedStore
5
5
  AR_VERSION = Gem::Version.new(ActiveRecord::VERSION::STRING)
6
6
  IS_AR_3_2 = AR_VERSION < Gem::Version.new('4.0')
7
- IS_AR_4_0 = AR_VERSION >= Gem::Version.new('4.0') && AR_VERSION < Gem::Version.new('4.1.0.beta')
8
7
  IS_AR_4_1 = AR_VERSION >= Gem::Version.new('4.1.0.beta')
9
8
 
10
- unless IS_AR_3_2
11
- ActiveModel::AttributeMethods::ClassMethods.send(:alias_method, :define_virtual_attribute_method, :define_attribute_method)
12
- end
13
-
14
9
  module Extension
15
10
  extend ActiveSupport::Concern
16
11
 
17
12
  included do
18
13
  class_attribute :stored_typed_attributes, instance_accessor: false
19
14
  self.stored_typed_attributes = {}
20
- require 'active_record/typed_store/ar_32_fallbacks' if IS_AR_3_2
21
- require 'active_record/typed_store/ar_41_fallbacks' if IS_AR_4_1
22
15
  end
23
16
 
24
17
  module ClassMethods
@@ -32,34 +25,72 @@ module ActiveRecord::TypedStore
32
25
  stored_typed_attributes[store_attribute].merge!(dsl.columns.index_by(&:name))
33
26
 
34
27
  dsl.column_names.each { |c| define_virtual_attribute_method(c.to_s) }
28
+ dsl.column_names.each { |c| define_store_attribute_queries(store_attribute, c) }
29
+
30
+ super(store_attribute, dsl) if defined?(super)
35
31
 
36
32
  dsl
37
33
  end
38
34
 
35
+ private
36
+
37
+ def define_store_attribute_queries(store_attribute, column_name)
38
+ define_method("#{column_name}?") do
39
+ query_store_attribute(store_attribute, column_name)
40
+ end
41
+ end
42
+
43
+ end
44
+
45
+ def reload(*)
46
+ reload_stores!
47
+ super
39
48
  end
40
49
 
41
50
  protected
42
51
 
43
52
  def write_store_attribute(store_attribute, key, value)
44
- casted_value = value
45
- if store_definition = self.class.stored_typed_attributes[store_attribute]
46
- if column_definition = store_definition[key]
47
- casted_value = column_definition.cast(value)
48
- end
49
- end
50
-
51
- attribute_will_change!(key.to_s) if casted_value != read_store_attribute(store_attribute, key)
53
+ previous_value = read_store_attribute(store_attribute, key)
54
+ casted_value = cast_store_attribute(store_attribute, key, value)
55
+ attribute_will_change!(key.to_s) if casted_value != previous_value
52
56
  super(store_attribute, key, casted_value)
53
57
  end
54
58
 
55
59
  private
56
60
 
61
+ def cast_store_attribute(store_attribute, key, value)
62
+ column = store_column_definition(store_attribute, key)
63
+ column ? column.cast(value) : value
64
+ end
65
+
66
+ def store_column_definition(store_attribute, key)
67
+ store_definition = self.class.stored_typed_attributes[store_attribute]
68
+ store_definition && store_definition[key]
69
+ end
70
+
71
+ def if_store_uninitialized(store_attribute)
72
+ initialized = "@_#{store_attribute}_initialized"
73
+ unless instance_variable_get(initialized)
74
+ yield
75
+ instance_variable_set(initialized, true)
76
+ end
77
+ end
78
+
79
+ def reload_stores!
80
+ self.class.stored_typed_attributes.keys.each do |store_attribute|
81
+ instance_variable_set("@_#{store_attribute}_initialized", false)
82
+ end
83
+ end
84
+
57
85
  def initialize_store_attribute(store_attribute)
58
- store = IS_AR_4_0 ? super : send(store_attribute)
59
- if columns = self.class.stored_typed_attributes[store_attribute]
60
- store = initialize_store(store, columns.values)
86
+ store = defined?(super) ? super : send(store_attribute)
87
+ store.tap do |store|
88
+ if_store_uninitialized(store_attribute) do
89
+ if columns = self.class.stored_typed_attributes[store_attribute]
90
+ initialize_store(store, columns.values)
91
+ end
92
+ end
61
93
  end
62
- store
63
94
  end
64
95
 
65
96
  def initialize_store(store, columns)
@@ -73,5 +104,36 @@ module ActiveRecord::TypedStore
73
104
  store
74
105
  end
75
106
 
107
+ # heavilly inspired from ActiveRecord::Base#query_attribute
108
+ def query_store_attribute(store_attribute, key)
109
+ value = read_store_attribute(store_attribute, key)
110
+
111
+ case value
112
+ when true then true
113
+ when false, nil then false
114
+ else
115
+ column = store_column_definition(store_attribute, key)
116
+ if column.nil?
117
+ if Numeric === value || value !~ /[^0-9]/
118
+ !value.to_i.zero?
119
+ else
120
+ return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
121
+ !value.blank?
122
+ end
123
+ elsif column.number?
124
+ !value.zero?
125
+ else
126
+ !value.blank?
127
+ end
128
+ end
129
+ end
130
+
76
131
  end
132
+
133
+ require 'active_record/typed_store/ar_32_fallbacks' if IS_AR_3_2
134
+ require 'active_record/typed_store/ar_41_fallbacks' if IS_AR_4_1
135
+ unless IS_AR_3_2
136
+ ActiveModel::AttributeMethods::ClassMethods.send(:alias_method, :define_virtual_attribute_method, :define_attribute_method)
137
+ end
138
+
77
139
  end
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module TypedStore
3
- VERSION = '0.2.0'
3
+ VERSION = '0.3.0'
4
4
  end
5
5
  end
@@ -0,0 +1 @@
1
+ require 'active_record/typed_store'
@@ -26,6 +26,19 @@ shared_examples 'any model' do
26
26
 
27
27
  end
28
28
 
29
+ describe 'attribute presence' do
30
+
31
+ it 'return true if present' do
32
+ expect(model.age?).to be_true
33
+ end
34
+
35
+ it 'return false if blank' do
36
+ model.age = 0
37
+ expect(model.age?).to be_false
38
+ end
39
+
40
+ end
41
+
29
42
  describe 'dirty tracking' do
30
43
 
31
44
  it 'track changed attributes' do
@@ -91,6 +104,10 @@ shared_examples 'any model' do
91
104
  expect(model.name).to be == ''
92
105
  end
93
106
 
107
+ it 'default to nil if specified explicitly' do
108
+ expect(model.cell_phone).to be_nil
109
+ end
110
+
94
111
  it 'properly cast the value as string' do
95
112
  model.update_attributes(name: 42)
96
113
  expect(model.reload.name).to be == '42'
@@ -325,6 +342,39 @@ shared_examples 'a store' do
325
342
 
326
343
  let(:model) { described_class.new }
327
344
 
345
+ describe 'initialization' do
346
+
347
+ it 'is done only once' do
348
+ model.should_receive(:initialize_store).once
349
+ 3.times do
350
+ model.age = (rand * 100).to_i
351
+ model.age
352
+ end
353
+ end
354
+
355
+ it 'is done again after a reload' do
356
+ model.save
357
+
358
+ model.should_receive(:initialize_store).twice
359
+ 3.times do
360
+ model.age = (rand * 100).to_i
361
+ model.age
362
+ end
363
+ model.reload
364
+ 3.times do
365
+ model.age = (rand * 100).to_i
366
+ model.age
367
+ end
368
+ end
369
+
370
+ it 'is not performe if no store attributes are accessed' do
371
+ model.should_not_receive(:initialize_store)
372
+ model.update_attributes(untyped_settings: {foo: :bar})
373
+ model.update_attributes(untyped_settings: {})
374
+ end
375
+
376
+ end
377
+
328
378
  describe 'attributes' do
329
379
 
330
380
  it 'retrieve default if assigned nil and null not allowed' do
@@ -438,7 +488,6 @@ shared_examples 'a model supporting arrays' do |regular=false|
438
488
  end
439
489
 
440
490
  describe Sqlite3RegularARModel do
441
- ActiveRecord::Base.establish_connection('test_sqlite3')
442
491
  it_should_behave_like 'any model'
443
492
  it_should_behave_like 'a db backed model'
444
493
  end
data/spec/spec_helper.rb CHANGED
@@ -11,7 +11,7 @@ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
11
11
  ]
12
12
  SimpleCov.start
13
13
 
14
- require 'active_record/typed_store'
14
+ require 'activerecord-typedstore'
15
15
 
16
16
  Dir[File.expand_path(File.join(File.dirname(__FILE__), 'support', '**', '*.rb'))].each { |f| require f }
17
17
 
@@ -13,6 +13,7 @@ def define_columns(t)
13
13
 
14
14
  t.string :name, default: '', null: false
15
15
  t.string :email
16
+ t.string :cell_phone, default: nil
16
17
 
17
18
  t.boolean :public, default: false, null: false
18
19
  t.boolean :enabled, default: true
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.2.0
4
+ version: 0.3.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-08 00:00:00.000000000 Z
11
+ date: 2013-11-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -161,6 +161,7 @@ files:
161
161
  - lib/active_record/typed_store/dsl.rb
162
162
  - lib/active_record/typed_store/extension.rb
163
163
  - lib/active_record/typed_store/version.rb
164
+ - lib/activerecord-typedstore.rb
164
165
  - spec/active_record/typed_store_spec.rb
165
166
  - spec/spec_helper.rb
166
167
  - spec/support/database_cleaner.rb