activerecord-typedstore 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +58 -0
- data/Rakefile +7 -0
- data/activerecord-typedstore.gemspec +28 -0
- data/gemfiles/Gemfile.ar-3.2 +8 -0
- data/gemfiles/Gemfile.ar-4.0 +8 -0
- data/gemfiles/Gemfile.ar-edge +8 -0
- data/lib/active_record/typed_store/column.rb +26 -0
- data/lib/active_record/typed_store/dsl.rb +24 -0
- data/lib/active_record/typed_store/extension.rb +79 -0
- data/lib/active_record/typed_store/version.rb +5 -0
- data/lib/active_record/typed_store.rb +11 -0
- data/spec/active_record/typed_store_spec.rb +294 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/database_cleaner.rb +13 -0
- data/spec/support/models.rb +81 -0
- metadata +152 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4903248fd9dd18108eb597807a28c894dddce203
|
4
|
+
data.tar.gz: 4ed8e3a9115d1cc1378eb013bc134c682cad5387
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ee405d97dacbd41d908f651bbdd5b6cebc7951d05923e09929ed30f085b17a2afa15d81bdae0a9790cb8fb817a02e273c7df27f216f724a206667baf2b55f7da
|
7
|
+
data.tar.gz: 566173ae233b5046cd24674d365bfdb4dd8db51753fefa9556f4c3ed8e234c56083d31c96f53e1a5ca71fc37c8eada3ac4eb9b31c7572239b285ed2d114a30ec
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Jean Boussier
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# ActiveRecord::TypedStore
|
2
|
+
|
3
|
+
[](http://travis-ci.org/byroot/activerecord-typedstore)
|
4
|
+
[](https://codeclimate.com/github/byroot/activerecord-typedstore)
|
5
|
+
|
6
|
+
[ActiveRecord::Store](http://api.rubyonrails.org/classes/ActiveRecord/Store.html) but with typed attributes.
|
7
|
+
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
gem 'activerecord-typedstore'
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install activerecord-typedstore
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
It works exactly like [ActiveRecord::Store documentation](http://api.rubyonrails.org/classes/ActiveRecord/Store.html) but you need to declare the type of your attributes.
|
26
|
+
|
27
|
+
Attributes definition is similar to activerecord's migrations:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
|
31
|
+
class Shop < ActiveRecord::Base
|
32
|
+
|
33
|
+
typed_store :settings do |s|
|
34
|
+
s.boolean :public, default: false
|
35
|
+
s.string :email, null: true
|
36
|
+
s.datetime :publish_at, null: true
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
shop = Shop.new(email: 'george@cyclim.se')
|
42
|
+
shop.public? # => false
|
43
|
+
shop.email # => 'george@cyclim.se'
|
44
|
+
shop.published_at # => nil
|
45
|
+
```
|
46
|
+
|
47
|
+
Type casting rules and attribute behavior are exactly the same as a for real database columns.
|
48
|
+
Actually the only difference is that you wont be able to query on these attributes (unless you use Postgres JSON or HStore types) and that you don't need to do a migration to add / remove an attribute.
|
49
|
+
|
50
|
+
If not please fill an issue.
|
51
|
+
|
52
|
+
## Contributing
|
53
|
+
|
54
|
+
1. Fork it
|
55
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
56
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
57
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
58
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'active_record/typed_store/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'activerecord-typedstore'
|
8
|
+
spec.version = ActiveRecord::TypedStore::VERSION
|
9
|
+
spec.authors = ['Jean Boussier']
|
10
|
+
spec.email = ['jean.boussier@gmail.com']
|
11
|
+
spec.description = %q{ActiveRecord::Store but with type definition}
|
12
|
+
spec.summary = %q{ActiveRecord::Store but with type definition}
|
13
|
+
spec.homepage = 'https://github.com/byroot/activerecord-typedstore'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_dependency 'activerecord', '>= 3.2'
|
22
|
+
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
24
|
+
spec.add_development_dependency 'rake'
|
25
|
+
spec.add_development_dependency 'rspec'
|
26
|
+
spec.add_development_dependency 'sqlite3'
|
27
|
+
spec.add_development_dependency 'database_cleaner'
|
28
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ActiveRecord::TypedStore
|
2
|
+
|
3
|
+
class Column < ::ActiveRecord::ConnectionAdapters::Column
|
4
|
+
|
5
|
+
def initialize(name, type, options={})
|
6
|
+
@name = name
|
7
|
+
@type = type
|
8
|
+
@default = extract_default(options.fetch(:default, nil))
|
9
|
+
@null = options.fetch(:null, false)
|
10
|
+
end
|
11
|
+
|
12
|
+
def type_cast(value)
|
13
|
+
if type == :string || type == :text
|
14
|
+
return value.to_s unless value.nil? && null
|
15
|
+
end
|
16
|
+
|
17
|
+
if IS_AR_3_2 && type == :datetime && value.is_a?(DateTime)
|
18
|
+
return super(value.iso8601)
|
19
|
+
end
|
20
|
+
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ActiveRecord::TypedStore
|
2
|
+
|
3
|
+
class DSL
|
4
|
+
|
5
|
+
attr_reader :columns
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@columns = []
|
9
|
+
yield self
|
10
|
+
end
|
11
|
+
|
12
|
+
def column_names
|
13
|
+
@columns.map(&:name)
|
14
|
+
end
|
15
|
+
|
16
|
+
[:string, :integer, :float, :decimal, :datetime, :date, :boolean].each do |type|
|
17
|
+
define_method(type) do |name, options={}|
|
18
|
+
@columns << Column.new(name, type, options)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'active_record/typed_store/column'
|
2
|
+
require 'active_record/typed_store/dsl'
|
3
|
+
|
4
|
+
module ActiveRecord::TypedStore
|
5
|
+
AR_VERSION = Gem::Version.new(ActiveRecord::VERSION::STRING)
|
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
|
+
IS_AR_4_1 = AR_VERSION >= Gem::Version.new('4.1.0.beta')
|
9
|
+
|
10
|
+
module Extension
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
included do
|
14
|
+
class_attribute :stored_typed_attributes, instance_accessor: false
|
15
|
+
self.stored_typed_attributes = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
|
20
|
+
def typed_store(store_attribute, options={}, &block)
|
21
|
+
dsl = DSL.new(&block)
|
22
|
+
|
23
|
+
store(store_attribute, options.merge(accessors: dsl.column_names))
|
24
|
+
|
25
|
+
stored_typed_attributes[store_attribute] ||= {}
|
26
|
+
stored_typed_attributes[store_attribute].merge!(dsl.columns.index_by(&:name))
|
27
|
+
|
28
|
+
if IS_AR_4_1 || IS_AR_3_2
|
29
|
+
after_initialize { initialize_store_attribute(store_attribute) }
|
30
|
+
end
|
31
|
+
|
32
|
+
if IS_AR_3_2
|
33
|
+
dsl.columns.each do |column|
|
34
|
+
define_method("#{column.name}_with_type_casting=") do |value|
|
35
|
+
self.send("#{column.name}_without_type_casting=", column.type_cast(value))
|
36
|
+
end
|
37
|
+
alias_method_chain "#{column.name}=", :type_casting
|
38
|
+
|
39
|
+
define_method(column.name) do
|
40
|
+
send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash)
|
41
|
+
store = send(store_attribute)
|
42
|
+
store.has_key?(column.name) ? store[column.name] : column.default
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
def write_store_attribute(store_attribute, key, value)
|
53
|
+
casted_value = value
|
54
|
+
if store_definition = self.class.stored_typed_attributes[store_attribute]
|
55
|
+
if column_definition = store_definition[key]
|
56
|
+
casted_value = column_definition.type_cast(value)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
super(store_attribute, key, casted_value)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def initialize_store_attribute(store_attribute)
|
65
|
+
attribute = IS_AR_4_0 ? super : send(store_attribute)
|
66
|
+
if columns = self.class.stored_typed_attributes[store_attribute]
|
67
|
+
columns.each do |name, definition|
|
68
|
+
if attribute.has_key?(name)
|
69
|
+
attribute[name] = definition.type_cast(attribute[name])
|
70
|
+
else
|
71
|
+
attribute[name] = definition.default if definition.has_default?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
attribute
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,294 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
ar_version = Gem::Version.new(ActiveRecord::VERSION::STRING)
|
4
|
+
ar_4_0 = Gem::Version.new('4.0')
|
5
|
+
ar_4_1 = Gem::Version.new('4.1.0.beta')
|
6
|
+
|
7
|
+
shared_examples 'a model' do
|
8
|
+
|
9
|
+
let(:model) { described_class.new }
|
10
|
+
|
11
|
+
describe 'build' do
|
12
|
+
|
13
|
+
it 'assign attributes received by #initialize' do
|
14
|
+
model = described_class.new(public: true)
|
15
|
+
expect(model.public).to be_true
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'unknown attribute' do
|
21
|
+
|
22
|
+
it 'raise an ActiveRecord::UnknownAttributeError on save attemps' do
|
23
|
+
expect {
|
24
|
+
model.update_attributes(unknown_attribute: 42)
|
25
|
+
}.to raise_error ActiveRecord::UnknownAttributeError
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'raise a NoMethodError on assignation attemps' do
|
29
|
+
expect {
|
30
|
+
model.unknown_attribute = 42
|
31
|
+
}.to raise_error NoMethodError
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'all attributes' do
|
37
|
+
|
38
|
+
it 'is initialized at nil if :default is not defined' do
|
39
|
+
expect(model.no_default).to be_nil
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'string attribute' do
|
45
|
+
|
46
|
+
it 'has the defined default as initial value' do
|
47
|
+
expect(model.name).to be == ''
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'properly cast the value as string' do
|
51
|
+
model.update_attributes(name: 42)
|
52
|
+
expect(model.reload.name).to be == '42'
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
describe 'boolean attribute' do
|
58
|
+
|
59
|
+
it 'has the defined :default as initial value' do
|
60
|
+
expect(model.public).to be_false
|
61
|
+
end
|
62
|
+
|
63
|
+
[true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON'].each do |value|
|
64
|
+
|
65
|
+
it "cast `#{value.inspect}` as `true`" do
|
66
|
+
model.public = value
|
67
|
+
expect(model.public).to be_true
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
[false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].each do |value|
|
73
|
+
|
74
|
+
it "cast `#{value.inspect}` as `false`" do
|
75
|
+
model.public = value
|
76
|
+
expect(model.public).to be_false
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'properly persit the value' do
|
82
|
+
model.update_attributes(public: false)
|
83
|
+
expect(model.reload.public).to be_false
|
84
|
+
model.update_attributes(public: true)
|
85
|
+
expect(model.reload.public).to be_true
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'initialize with default value if the column is not nullable' do
|
89
|
+
expect(model.public).to be_false
|
90
|
+
model.save
|
91
|
+
expect(model.reload.public).to be_false
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'can store nil if the column is nullable' do
|
95
|
+
model.update_attributes(enabled: nil)
|
96
|
+
expect(model.reload.enabled).to be_nil
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'save the default value if the column is nullable but the value not explictly set' do
|
100
|
+
model.save
|
101
|
+
expect(model.reload.enabled).to be_true
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
describe 'integer attributes' do
|
107
|
+
|
108
|
+
it 'has the defined default as initial value' do
|
109
|
+
expect(model.age).to be_zero
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'properly cast assigned value to integer' do
|
113
|
+
model.age = '42'
|
114
|
+
expect(model.age).to be == 42
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'properly cast non numeric values to integer' do
|
118
|
+
model.age = 'foo'
|
119
|
+
expect(model.age).to be == 0
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'can store nil if the column is nullable' do
|
123
|
+
model.update_attributes(max_length: nil)
|
124
|
+
expect(model.reload.max_length).to be_nil
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
describe 'float attributes' do
|
130
|
+
|
131
|
+
it 'has the defined default as initial value' do
|
132
|
+
expect(model.rate).to be_zero
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'properly cast assigned value to float' do
|
136
|
+
model.rate = '4.2'
|
137
|
+
expect(model.rate).to be == 4.2
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'properly cast non numeric values to float' do
|
141
|
+
model.rate = 'foo'
|
142
|
+
expect(model.rate).to be == 0
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'can store nil if the column is nullable' do
|
146
|
+
model.update_attributes(price: nil)
|
147
|
+
expect(model.reload.price).to be_nil
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
describe 'decimal attributes' do
|
153
|
+
|
154
|
+
it 'has the defined default as initial value' do
|
155
|
+
expect(model.total_price).to be == BigDecimal.new('4.2')
|
156
|
+
expect(model.total_price).to be_a BigDecimal
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'properly cast assigned value to decimal' do
|
160
|
+
model.shipping_cost = 4.2
|
161
|
+
expect(model.shipping_cost).to be == BigDecimal.new('4.2')
|
162
|
+
expect(model.shipping_cost).to be_a BigDecimal
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'properly cast non numeric values to decimal' do
|
166
|
+
model.total_price = 'foo'
|
167
|
+
expect(model.total_price).to be == 0
|
168
|
+
expect(model.total_price).to be_a BigDecimal
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'retreive a BigDecimal instance' do
|
172
|
+
model.update_attributes(shipping_cost: 4.2)
|
173
|
+
expect(model.reload.shipping_cost).to be == BigDecimal.new('4.2')
|
174
|
+
expect(model.reload.shipping_cost).to be_a BigDecimal
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'can store nil if the column is nullable' do
|
178
|
+
model.update_attributes(shipping_cost: nil)
|
179
|
+
expect(model.reload.shipping_cost).to be_nil
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
describe 'date attributes' do
|
185
|
+
|
186
|
+
let(:date) { Date.new(1984, 6, 8) }
|
187
|
+
|
188
|
+
it 'has the defined default as initial value' do
|
189
|
+
expect(model.published_on).to be == date
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'properly cast assigned value to date' do
|
193
|
+
model.remind_on = '1984-06-08'
|
194
|
+
expect(model.remind_on).to be == date
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'retreive a Date instance' do
|
198
|
+
model.update_attributes(published_on: date)
|
199
|
+
expect(model.reload.published_on).to be == date
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'nillify unparsable dates' do
|
203
|
+
model.published_on = 'foo'
|
204
|
+
expect(model.published_on).to be_nil
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'can store nil if the column is nullable' do
|
208
|
+
model.update_attributes(remind_on: nil)
|
209
|
+
expect(model.reload.remind_on).to be_nil
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
213
|
+
|
214
|
+
describe 'datetime attributes' do
|
215
|
+
|
216
|
+
let(:datetime) { DateTime.new(1984, 6, 8, 13, 57, 12) }
|
217
|
+
let(:datetime_string) { '1984-06-08 13:57:12' }
|
218
|
+
let(:time) { Time.parse(datetime_string) }
|
219
|
+
|
220
|
+
context "with ActiveRecord #{ActiveRecord::VERSION::STRING}" do
|
221
|
+
|
222
|
+
if ar_version < ar_4_0
|
223
|
+
|
224
|
+
it 'has the defined default as initial value' do
|
225
|
+
model.save
|
226
|
+
expect(model.published_at).to be == time
|
227
|
+
end
|
228
|
+
|
229
|
+
it 'properly cast assigned value to time' do
|
230
|
+
model.remind_at = datetime_string
|
231
|
+
expect(model.remind_at).to be == time
|
232
|
+
end
|
233
|
+
|
234
|
+
it 'properly cast assigned value to time on save' do
|
235
|
+
model.remind_at = datetime_string
|
236
|
+
model.save
|
237
|
+
model.reload
|
238
|
+
expect(model.remind_at).to be == time
|
239
|
+
end
|
240
|
+
|
241
|
+
it 'retreive a Time instance' do
|
242
|
+
model.update_attributes(published_at: datetime)
|
243
|
+
expect(model.reload.published_at).to be == time
|
244
|
+
end
|
245
|
+
|
246
|
+
else
|
247
|
+
|
248
|
+
it 'has the defined default as initial value' do
|
249
|
+
model.save
|
250
|
+
expect(model.reload.published_at).to be == datetime
|
251
|
+
end
|
252
|
+
|
253
|
+
it 'retreive a DateTime instance' do
|
254
|
+
model.update_attributes(published_at: datetime)
|
255
|
+
expect(model.reload.published_at).to be == datetime
|
256
|
+
end
|
257
|
+
|
258
|
+
it 'properly cast assigned value to datetime' do
|
259
|
+
model.remind_at = datetime_string
|
260
|
+
expect(model.remind_at).to be == datetime
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
265
|
+
|
266
|
+
it 'nillify unparsable datetimes' do
|
267
|
+
model.published_at = 'foo'
|
268
|
+
expect(model.published_at).to be_nil
|
269
|
+
end
|
270
|
+
|
271
|
+
it 'can store nil if the column is nullable' do
|
272
|
+
model.update_attributes(remind_at: nil)
|
273
|
+
expect(model.reload.remind_at).to be_nil
|
274
|
+
end
|
275
|
+
|
276
|
+
end
|
277
|
+
|
278
|
+
end
|
279
|
+
|
280
|
+
describe RegularARModel do
|
281
|
+
it_should_behave_like 'a model'
|
282
|
+
end
|
283
|
+
|
284
|
+
describe YamlTypedStoreModel do
|
285
|
+
it_should_behave_like 'a model'
|
286
|
+
end
|
287
|
+
|
288
|
+
describe JsonTypedStoreModel do
|
289
|
+
it_should_behave_like 'a model'
|
290
|
+
end
|
291
|
+
|
292
|
+
describe MarshalTypedStoreModel do
|
293
|
+
it_should_behave_like 'a model'
|
294
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
require 'database_cleaner'
|
5
|
+
require 'active_record/typed_store'
|
6
|
+
|
7
|
+
Dir[File.expand_path(File.join(File.dirname(__FILE__), 'support', '**', '*.rb'))].each { |f| require f }
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
11
|
+
config.run_all_when_everything_filtered = true
|
12
|
+
config.filter_run :focus
|
13
|
+
config.order = 'random'
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
DatabaseCleaner[:active_record].strategy = :transaction
|
2
|
+
|
3
|
+
RSpec.configure do |config|
|
4
|
+
config.before :suite do
|
5
|
+
DatabaseCleaner.clean_with :transaction
|
6
|
+
end
|
7
|
+
config.before :each do
|
8
|
+
DatabaseCleaner.start
|
9
|
+
end
|
10
|
+
config.after :each do
|
11
|
+
DatabaseCleaner.clean
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'json'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
ActiveRecord::Base.configurations = {'test' => {:adapter => 'sqlite3', :database => ':memory:'}}
|
6
|
+
ActiveRecord::Base.establish_connection('test')
|
7
|
+
|
8
|
+
def define_columns(t)
|
9
|
+
t.integer :no_default
|
10
|
+
|
11
|
+
t.string :name, default: ''
|
12
|
+
t.string :email, null: true
|
13
|
+
|
14
|
+
t.boolean :public, default: false
|
15
|
+
t.boolean :enabled, default: true, null: true
|
16
|
+
|
17
|
+
t.integer :age, default: 0
|
18
|
+
t.integer :max_length, null: true
|
19
|
+
|
20
|
+
t.float :rate, default: 0
|
21
|
+
t.float :price, null: true
|
22
|
+
|
23
|
+
t.date :published_on, default: '1984-06-08'
|
24
|
+
t.date :remind_on, null: true
|
25
|
+
|
26
|
+
t.datetime :published_at, default: '1984-06-08 13:57:12'
|
27
|
+
t.datetime :remind_at, null: true
|
28
|
+
|
29
|
+
t.decimal :total_price, default: 4.2
|
30
|
+
t.decimal :shipping_cost, null: true
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
class CreateAllTables < ActiveRecord::Migration
|
35
|
+
def self.up
|
36
|
+
create_table(:regular_ar_models) { |t| define_columns(t) }
|
37
|
+
create_table(:yaml_typed_store_models) { |t| t.text :settings }
|
38
|
+
create_table(:json_typed_store_models) { |t| t.text :settings }
|
39
|
+
create_table(:marshal_typed_store_models) { |t| t.text :settings }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
ActiveRecord::Migration.verbose = false
|
43
|
+
CreateAllTables.up
|
44
|
+
|
45
|
+
class RegularARModel < ActiveRecord::Base
|
46
|
+
end
|
47
|
+
|
48
|
+
class YamlTypedStoreModel < ActiveRecord::Base
|
49
|
+
typed_store :settings do |s|
|
50
|
+
define_columns(s)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class ColumnCoder
|
55
|
+
|
56
|
+
def initialize(coder)
|
57
|
+
@coder = coder
|
58
|
+
end
|
59
|
+
|
60
|
+
def load(data)
|
61
|
+
return {} unless data
|
62
|
+
@coder.load(data)
|
63
|
+
end
|
64
|
+
|
65
|
+
def dump(data)
|
66
|
+
@coder.dump(data || {})
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
class JsonTypedStoreModel < ActiveRecord::Base
|
72
|
+
typed_store :settings, coder: ColumnCoder.new(JSON) do |s|
|
73
|
+
define_columns(s)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class MarshalTypedStoreModel < ActiveRecord::Base
|
78
|
+
typed_store :settings, coder: ColumnCoder.new(Marshal) do |s|
|
79
|
+
define_columns(s)
|
80
|
+
end
|
81
|
+
end
|
metadata
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-typedstore
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jean Boussier
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-10-27 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: '3.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
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: database_cleaner
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: ActiveRecord::Store but with type definition
|
98
|
+
email:
|
99
|
+
- jean.boussier@gmail.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- .gitignore
|
105
|
+
- .rspec
|
106
|
+
- .travis.yml
|
107
|
+
- Gemfile
|
108
|
+
- LICENSE.txt
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- activerecord-typedstore.gemspec
|
112
|
+
- gemfiles/Gemfile.ar-3.2
|
113
|
+
- gemfiles/Gemfile.ar-4.0
|
114
|
+
- gemfiles/Gemfile.ar-edge
|
115
|
+
- lib/active_record/typed_store.rb
|
116
|
+
- lib/active_record/typed_store/column.rb
|
117
|
+
- lib/active_record/typed_store/dsl.rb
|
118
|
+
- lib/active_record/typed_store/extension.rb
|
119
|
+
- lib/active_record/typed_store/version.rb
|
120
|
+
- spec/active_record/typed_store_spec.rb
|
121
|
+
- spec/spec_helper.rb
|
122
|
+
- spec/support/database_cleaner.rb
|
123
|
+
- spec/support/models.rb
|
124
|
+
homepage: https://github.com/byroot/activerecord-typedstore
|
125
|
+
licenses:
|
126
|
+
- MIT
|
127
|
+
metadata: {}
|
128
|
+
post_install_message:
|
129
|
+
rdoc_options: []
|
130
|
+
require_paths:
|
131
|
+
- lib
|
132
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - '>='
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
requirements: []
|
143
|
+
rubyforge_project:
|
144
|
+
rubygems_version: 2.1.4
|
145
|
+
signing_key:
|
146
|
+
specification_version: 4
|
147
|
+
summary: ActiveRecord::Store but with type definition
|
148
|
+
test_files:
|
149
|
+
- spec/active_record/typed_store_spec.rb
|
150
|
+
- spec/spec_helper.rb
|
151
|
+
- spec/support/database_cleaner.rb
|
152
|
+
- spec/support/models.rb
|