active_store_accessor 0.2.1 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3d80e0a2c9f3bf1d6fde04802b05c5d01f89f179
4
- data.tar.gz: 92665c56d16926a3b909c0d45650f0a6032ffe26
3
+ metadata.gz: 3b01aee96903d6965851b7fe623f388deeae0bf2
4
+ data.tar.gz: 46265923f7da2f21da4cac1f6ccdaf164842ec73
5
5
  SHA512:
6
- metadata.gz: b91de091930c91921061b8dd70713b3c568bb6e4c8c29009fa191f49ceac194782e27e2c3132cbff2ac542b97b6e563da4d54e73a5bd7e6d94772a567f29ed87
7
- data.tar.gz: 82ea8763e58340fe731de04f2db72a2f6ed6bab7e1b1bfbba98199e89b9fee93a7e58639943bf8279f29734b2aeb2368951947b242690ecbd644583b9ac4f17d
6
+ metadata.gz: e166097b7ad612555e65b6693c6915224bc97a7d0a656349a7c5ba6084178a65399e16126e91a8fafd10a48dd4402edb2815c0e161e3dd2a63b5c87d75be7f89
7
+ data.tar.gz: 38594357276667ebf2e2728f6cb34b8ef31978beddb79bc95a19e13f8b0b4be00f65c745a593b28e57f995fd014dd696e2ab24c8bafd89f3bb4eb382836dac76
data/README.md CHANGED
@@ -4,15 +4,14 @@
4
4
  [![Gem Version](https://badge.fury.io/rb/active_store_accessor.svg)](http://badge.fury.io/rb/active_store_accessor)
5
5
  [![Code Climate](https://codeclimate.com/github/jalkoby/active_store_accessor.png)](https://codeclimate.com/github/jalkoby/active_store_accessor)
6
6
 
7
- `active_store_accessor` makes work with store accessors more productive. There is no need to cast a serialized attribute to required type(boolean, time, float, etc). Just define it with a tiny wrapper method and everything is done for you.
7
+ `active_store_accessor` makes a work with store accessors more productive. There is no need to cast a serialized attribute to a required type(boolean, time, float, etc). Just define it with a tiny wrapper method and everything is done for you.
8
8
 
9
9
  ## Usage
10
10
 
11
- Basic use case:
11
+ The basic usage:
12
12
 
13
13
  ```ruby
14
14
  class Profile < ActiveRecord::Base
15
- # basic usage(where `info` is a store column)
16
15
  active_store_accessor :info, age: :integer, birthday: :time
17
16
 
18
17
  # with default values
@@ -30,11 +29,11 @@ profile.score = 4.5
30
29
  profile.score # => 4.5
31
30
  ```
32
31
 
33
- Extra logic in property methods:
32
+ The extra logic in a property methods:
34
33
  ```ruby
35
34
  # Story:
36
- # users have a rank, but if an user was locked by admins
37
- # nobody can change it rank & it's value should be equal to zero
35
+ # users have a rank, but if a user was locked by admins
36
+ # nobody can change a rank & it's value should be equal to zero
38
37
  class User
39
38
  active_store_accessor :info, rank: :float
40
39
 
@@ -49,6 +48,72 @@ class User
49
48
  end
50
49
  ```
51
50
 
51
+ ## Adding a custom type
52
+ Add a custom type is easy enough:
53
+
54
+ ```ruby
55
+ # using a block
56
+ ActiveStoreAccessor.add_type(:even) do |builder|
57
+ builder.to_source { |value| (value.to_i / 2) * 2 }
58
+ end
59
+
60
+ # using a lambda
61
+ ActiveStoreAccessor.add_type(:even) do |builder|
62
+ to_source = lambda { |value| (value.to_i / 2) * 2 }
63
+ builder.to_source(to_source)
64
+ end
65
+
66
+ # using a object with #call method
67
+ class EvenConvert
68
+ def call(value)
69
+ (value.to_i / 2) * 2
70
+ end
71
+ end
72
+
73
+ ActiveStoreAccessor.add_type(:even) do |builder|
74
+ builder.to_source(EvenConvert.new)
75
+ end
76
+ ```
77
+
78
+ Sometimes you need to deserialize your value of a custom type. To do it look at the following example:
79
+
80
+ ```ruby
81
+ ActiveStoreAccessor.add_type(:point) do |builder|
82
+ builder.to_source do |value|
83
+ "#{ value.x },#{ value.y }"
84
+ end
85
+
86
+ builder.from_source do |value|
87
+ parts = value.split(',')
88
+ Point.new(parts[0], parts[1])
89
+ end
90
+ end
91
+ ```
92
+
93
+ There is a common issue when you use `block`-style to define a custom type:
94
+
95
+ ```ruby
96
+ ActiveStoreAccessor.add_type(:point) |builder|
97
+ builder.to_source do |value|
98
+ return unless value.is_a?(Point)
99
+ # ...
100
+ end
101
+ end
102
+ ```
103
+
104
+ Ruby will rise an error Unexpected Return (LocalJumpError). To avoid it replace a block by a lambda:
105
+
106
+ ```ruby
107
+ ActiveStoreAccessor.add_type(:point) |builder|
108
+ to_source = lambda do |value|
109
+ return unless value.is_a?(Point)
110
+ # ...
111
+ end
112
+
113
+ builder.to_source(to_source)
114
+ end
115
+ ```
116
+
52
117
  ## Installation
53
118
 
54
119
  Add this line to your application's Gemfile:
@@ -65,7 +130,7 @@ This library has been tested on ruby 1.9.3+ and activerecord 4.0+.
65
130
 
66
131
  ## Contributing
67
132
 
68
- 1. Fork it ( https://github.com/[my-github-username]/active_store_accessor/fork )
133
+ 1. Fork it ( https://github.com/jalkoby/active_store_accessor/fork )
69
134
  2. Create your feature branch (`git checkout -b my-new-feature`)
70
135
  3. Commit your changes (`git commit -am 'Add some feature'`)
71
136
  4. Push to the branch (`git push origin my-new-feature`)
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "active_store_accessor"
7
- spec.version = "0.2.1"
7
+ spec.version = "0.3.1"
8
8
  spec.authors = ["Sergey Pchelincev"]
9
9
  spec.email = ["mail@sergeyp.me"]
10
10
  spec.summary = %q{Get more from ActiveRecord::Store}
@@ -1,63 +1,140 @@
1
+ require_relative "active_store_accessor/builder"
2
+ require_relative "active_store_accessor/rails"
3
+ require_relative "active_store_accessor/type"
4
+
1
5
  module ActiveStoreAccessor
2
- def active_store_attributes
3
- return @active_store_attributes if @active_store_attributes
6
+ extend self
7
+
8
+ def types
9
+ @types ||= []
10
+ end
4
11
 
5
- @active_store_attributes = superclass.respond_to?(:active_store_attributes) ? superclass.active_store_attributes : {}
12
+ def add_type(*names)
13
+ raise_arg_error("You could use only symbols for name") if names.any? { |name| !name.is_a?(Symbol) }
14
+ builder = Builder.new(names)
15
+ yield builder
16
+ types << builder.to_type
6
17
  end
7
18
 
8
- def active_store_accessor(column_name, attrs)
9
- column = columns.detect { |column| column.name == column_name.to_s }
10
- if !column
11
- raise "[active_store_accessor] The column '#{column_name}' does not exist in the model #{name}."
12
- elsif column.type == :text
13
- serialize(column_name) unless serialized_attributes.include?(column_name.to_s)
19
+ def find_type(name, options, model_name)
20
+ raise_arg_error("Please use a symbol for a setting type in #{ model_name }.") unless name.is_a?(Symbol)
21
+ type_args = types.detect { |type_args| type_args[0].include?(name) }
22
+ unless type_args
23
+ raise_arg_error("#{ name } is unknown. Please recheck a type's name or add it by `ActiveStoreAccessor.add_type`.")
14
24
  end
25
+ Type.new(type_args[1], type_args[2], options[:default])
26
+ end
27
+
28
+ def raise_error(klass, message)
29
+ raise klass, "[active_store_accessor] #{ message }"
30
+ end
15
31
 
16
- store_accessor column_name, *attrs.keys
32
+ def raise_arg_error(message)
33
+ raise_error ArgumentError, message
34
+ end
17
35
 
18
- attrs.each do |attr_name, options|
19
- options = { type: options.to_s } unless options.is_a?(Hash)
20
- type = options.fetch(:type) { raise ArgumentError, "please specify type of `#{ attr_name }` attribute" }.to_s
36
+ def raise_convertion_error(value, type)
37
+ raise_error klass, "ActiveStoreAccessor doesn't know how to convert #{ value.class } into #{ type }"
38
+ end
39
+
40
+ add_type(:integer) do |builder|
41
+ builder.to_source { |value| value.to_i rescue nil }
42
+ end
21
43
 
22
- config = if connection.respond_to?(:lookup_cast_type)
23
- column = connection.lookup_cast_type(type)
24
- [column.method(:type_cast_from_database), column.method(:type_cast_for_database)]
44
+ add_type(:float, :double) do |builder|
45
+ builder.to_source { |value| value.to_f unless value.nil? }
46
+ end
47
+
48
+ add_type(:decimal) do |builder|
49
+ builder.from_source { |value| value.to_d unless value.nil? }
50
+
51
+ builder.to_source do |value|
52
+ if value.respond_to?(:to_d)
53
+ value.to_d.to_s
25
54
  else
26
- # time for activerecord is only a hours and minutes without date part
27
- # but for most rubist and rails developer it should contains a date too
28
- type = 'datetime' if type == 'time'
29
- args = [attr_name.to_s, options[:default], type]
30
- column = ActiveRecord::ConnectionAdapters::Column.new(*args)
31
- [column.method(:type_cast), column.method(:type_cast_for_write)]
55
+ value.to_s.presence
32
56
  end
57
+ end
58
+ end
33
59
 
34
- config << options[:default]
35
- active_store_attributes[attr_name] = config
60
+ # time for activerecord is only a hours and minutes without date part
61
+ # but for most rubist and rails developer it should contains a date too
62
+ add_type(:time, :datetime) do |builder|
63
+ builder.from_source { |value| Time.parse(value) if value }
36
64
 
37
- _active_store_accessor_module.module_eval <<-RUBY
38
- def #{ attr_name }
39
- getter, _, default = self.class.active_store_attributes[:#{ attr_name }]
40
- value = getter.call(super)
41
- value.nil? ? default : value
42
- end
65
+ to_source = lambda do |value|
66
+ return unless value.present?
43
67
 
44
- def #{ attr_name }=(value)
45
- _, setter, _ = self.class.active_store_attributes[:#{ attr_name }]
46
- super setter.call(value)
47
- end
48
- RUBY
68
+ case value
69
+ when String then Time.parse(value).to_s
70
+ when Fixnum then Time.at(value).to_s
71
+ when Date, Time, DateTime, ActiveSupport::TimeZone then value.to_s
72
+ else
73
+ raise_convertion_error(value, "Time")
74
+ end
49
75
  end
76
+
77
+ builder.to_source(to_source)
78
+ end
79
+
80
+ add_type(:date) do |builder|
81
+ to_source = lambda do |value|
82
+ return if value.nil?
83
+ case value
84
+ when String then Date.parse(value).to_s
85
+ when Date then value.to_s
86
+ when Time, DateTime then value.to_date.to_s
87
+ else
88
+ raise_convertion_error(value, "Date")
89
+ end
90
+ end
91
+
92
+ builder.to_source(to_source)
50
93
  end
51
94
 
52
- private
95
+ add_type(:text, :string, :binary) do |builder|
96
+ builder.to_source { |value| value.presence }
97
+ end
53
98
 
54
- def _active_store_accessor_module
55
- @_active_store_accessor_module ||= begin
56
- mod = Module.new
57
- include mod
58
- mod
99
+ add_type(:boolean, :bool) do |builder|
100
+ true_values = [true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON'].to_set
101
+
102
+ builder.to_source do |value|
103
+ if value.nil? || (value.is_a?(String) && value.empty?)
104
+ nil
105
+ else
106
+ true_values.include?(value)
107
+ end
108
+ end
109
+ end
110
+
111
+ add_type(:array, :list) do |builder|
112
+ builder.to_source { |value| ActiveSupport::JSON.encode(Array(value)) unless value.nil? }
113
+ builder.from_source { |value| ActiveSupport::JSON.decode(value) unless value.nil? }
114
+ end
115
+
116
+ add_type(:hash, :dictonary) do |builder|
117
+ if RUBY_VERSION =~ /1\.9/
118
+ to_source = lambda do |value|
119
+ return if value.nil?
120
+
121
+ value = if value.is_a?(Hash)
122
+ value
123
+ elsif value.respond_to?(:to_h)
124
+ value.to_h
125
+ elsif value.respond_to?(:to_hash)
126
+ value.to_hash
127
+ else
128
+ raise_convertion_error(value, 'Hash')
129
+ end
130
+ ActiveSupport::JSON.encode(value)
131
+ end
132
+ builder.to_source(to_source)
133
+ else
134
+ builder.to_source { |value| ActiveSupport::JSON.encode(Hash(value)) unless value.nil? }
59
135
  end
136
+ builder.from_source { |value| ActiveSupport::JSON.decode(value) unless value.nil? }
60
137
  end
61
138
  end
62
139
 
63
- ActiveSupport.on_load(:active_record) { ActiveRecord::Base.extend(ActiveStoreAccessor) }
140
+ ActiveSupport.on_load(:active_record) { ActiveRecord::Base.extend(ActiveStoreAccessor::Rails) }
@@ -0,0 +1,21 @@
1
+ module ActiveStoreAccessor
2
+ class Builder
3
+ DEFAULT_CASTER = lambda { |value| value }
4
+
5
+ def initialize(names)
6
+ @names = names
7
+ end
8
+
9
+ def from_source(caster = nil, &block)
10
+ @from_source = caster || block
11
+ end
12
+
13
+ def to_source(caster = nil, &block)
14
+ @to_source = caster || block
15
+ end
16
+
17
+ def to_type
18
+ [@names, (@from_source || DEFAULT_CASTER), (@to_source || DEFAULT_CASTER)]
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,50 @@
1
+ module ActiveStoreAccessor
2
+ module Rails
3
+ def active_store_attributes
4
+ return @active_store_attributes if @active_store_attributes
5
+
6
+ @active_store_attributes = superclass.respond_to?(:active_store_attributes) ? superclass.active_store_attributes : {}
7
+ end
8
+
9
+ def active_store_accessor(column_name, attrs)
10
+ column = columns.detect { |column| column.name == column_name.to_s }
11
+ if !column
12
+ ::ActiveStoreAccessor.raise_arg_error "The column `#{ column_name }` does not exist in the model #{ name }."
13
+ elsif column.type == :text
14
+ serialize(column_name) unless serialized_attributes.include?(column_name.to_s)
15
+ end
16
+
17
+ store_accessor column_name, *attrs.keys
18
+
19
+ attrs.each do |attr_name, options|
20
+ options = { type: options } unless options.is_a?(Hash)
21
+
22
+ type = options.fetch(:type) do
23
+ ::ActiveStoreAccessor.raise_arg_error "Please specify the type of `#{ attr_name }` attribute."
24
+ end
25
+
26
+ active_store_attributes[attr_name] = ::ActiveStoreAccessor.find_type(type, options, name)
27
+
28
+ _active_store_accessor_module.module_eval <<-RUBY
29
+ def #{ attr_name }
30
+ self.class.active_store_attributes.fetch(:#{ attr_name }).from_store(super)
31
+ end
32
+
33
+ def #{ attr_name }=(value)
34
+ super self.class.active_store_attributes.fetch(:#{ attr_name }).to_store(value)
35
+ end
36
+ RUBY
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def _active_store_accessor_module
43
+ @_active_store_accessor_module ||= begin
44
+ mod = Module.new
45
+ include mod
46
+ mod
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,18 @@
1
+ module ActiveStoreAccessor
2
+ class Type
3
+ def initialize(from, to, default)
4
+ @from = from
5
+ @to = to
6
+ @default = default
7
+ end
8
+
9
+ def from_store(value)
10
+ result = @from.call(value)
11
+ result.nil? ? @default : result
12
+ end
13
+
14
+ def to_store(value)
15
+ @to.call(value)
16
+ end
17
+ end
18
+ end
@@ -9,18 +9,24 @@ describe ActiveStoreAccessor do
9
9
  assert_equal profile.rank, nil
10
10
  assert_equal profile.birthday, nil
11
11
  assert_equal profile.confirmed, nil
12
+ assert_equal profile.photos, nil
13
+ assert_equal profile.scores, nil
12
14
 
13
15
  profile.age = "20"
14
16
  profile.score = 100.32
15
17
  profile.rank = "3213.317"
16
18
  profile.birthday = Time.utc(2014, 5, 12)
17
19
  profile.confirmed = "1"
20
+ profile.photos = 'my_doggy.png'
21
+ profile.scores = { woo: 2 }
18
22
 
19
23
  assert_equal profile.age, 20
20
24
  assert_equal profile.score, 100
21
25
  assert_equal profile.rank, 3213.32
22
26
  assert_equal profile.birthday, Time.utc(2014, 5, 12)
23
27
  assert profile.confirmed
28
+ assert_equal profile.photos, ['my_doggy.png']
29
+ assert_equal profile.scores, { 'woo' => 2 }
24
30
  end
25
31
 
26
32
  it "should support model inheritance" do
@@ -25,6 +25,8 @@ class Profile < ActiveRecord::Base
25
25
 
26
26
  active_store_accessor :keys, active: :boolean
27
27
  active_store_accessor :keys, pi: :float
28
+ active_store_accessor :keys, photos: :list
29
+ active_store_accessor :keys, scores: :hash
28
30
 
29
31
  def rank=(value)
30
32
  super(value)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_store_accessor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergey Pchelincev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-11-19 00:00:00.000000000 Z
11
+ date: 2015-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -81,6 +81,9 @@ files:
81
81
  - Rakefile
82
82
  - active_store_accessor.gemspec
83
83
  - lib/active_store_accessor.rb
84
+ - lib/active_store_accessor/builder.rb
85
+ - lib/active_store_accessor/rails.rb
86
+ - lib/active_store_accessor/type.rb
84
87
  - tests/lib/active_store_accessor_test.rb
85
88
  - tests/test_helper.rb
86
89
  homepage: ''