active_store_accessor 0.2.1 → 0.3.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 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: ''