attrio 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  # Attrio
8
8
 
9
- Attributes for plain Ruby objects. The goal is to provide an ability to define attributes for your models without reinventing the wheel all over again. Attrio doesn't have any third-party dependencies like Virtus or ActiveAttr and does not redefine any methods inside your class, unless you want it to.
9
+ Attributes for plain Ruby objects. The goal is to provide an ability to define attributes for your models without reinventing the wheel all over again. Attrio doesn't have any third-party dependencies like [Virtus](https://github.com/solnic/virtus) or [ActiveAttr](https://github.com/cgriego/active_attr) and does not redefine any methods inside your class, unless you want it to.
10
10
 
11
11
  ## Installation
12
12
 
@@ -38,7 +38,7 @@ class User
38
38
  end
39
39
  ```
40
40
 
41
- By default Attrio creates `#attributes` accessor which contains `Hash` of with attributes names as keys and instances of `Attrio::Attribute` as values. Each instance of `Attrio::Attribute` contains following information:
41
+ By default Attrio defines `#attributes` accessor which contains `Hash` with attributes names as keys and instances of `Attrio::Attribute` as values. Each instance of `Attrio::Attribute` contains following information:
42
42
  * type
43
43
  * writer method name
44
44
  * writer method visibility
@@ -57,7 +57,7 @@ user.attributes
57
57
  # }
58
58
  ```
59
59
 
60
- Accessor name can be easily overriden by passing `:as` option to `define_attributes` block.
60
+ Accessor name can be easily overridden by passing `:as` option to `define_attributes` block.
61
61
 
62
62
  ```ruby
63
63
  class User
@@ -76,6 +76,120 @@ user = User.new
76
76
  user.api_attributes # => {...}
77
77
  ```
78
78
 
79
+ ### Default values
80
+
81
+ Attrio supports all the ways to setup default values that Virtus has.
82
+
83
+ ```ruby
84
+ class Page
85
+ include Attrio
86
+
87
+ define_attributes do
88
+ attr :title, String
89
+
90
+ # default from a singleton value (integer in this case)
91
+ attr :views, Integer, :default => 0
92
+
93
+ # default from a singleton value (boolean in this case)
94
+ attr :published, Boolean, :default => false
95
+
96
+ # default from a callable object (proc in this case)
97
+ attr :slug, String, :default => lambda { |page, attribute| page.title.present? ? page.title.downcase.gsub(' ', '-') : nil }
98
+
99
+ # default from a method name as symbol
100
+ attr :editor_title, String, :default => :default_editor_title
101
+ end
102
+
103
+ def initialize(attributes = {})
104
+ self.attributes = attributes
105
+ end
106
+
107
+ def attributes=(attributes = {})
108
+ attributes.each do |attr,value|
109
+ self.send("#{attr}=", value) if self.respond_to?("#{attr}=")
110
+ end
111
+ end
112
+
113
+ def default_editor_title
114
+ if self.published?
115
+ title
116
+ else
117
+ title.present? ? "UNPUBLISHED: #{title}" : "UNPUBLISHED"
118
+ end
119
+ end
120
+ end
121
+ ```
122
+
123
+ ### Embed Value
124
+ You can embed values in Attrio just like you do it in Virtus.
125
+
126
+ ```ruby
127
+ module MassAssignment
128
+ def initialize(attributes = {})
129
+ self.attributes = attributes
130
+ end
131
+
132
+ def attributes=(attributes = {})
133
+ attributes.each do |attr,value|
134
+ self.send("#{attr}=", value) if self.respond_to?("#{attr}=")
135
+ end
136
+ end
137
+ end
138
+
139
+ class City
140
+ include Attrio
141
+ include MassAssignment
142
+
143
+ define_attributes do
144
+ attr :name, String
145
+ end
146
+ end
147
+
148
+ class Address
149
+ include Attrio
150
+ include MassAssignment
151
+
152
+ define_attributes do
153
+ attr :street, String
154
+ attr :zipcode, String
155
+ attr :city, City
156
+ end
157
+ end
158
+
159
+ class User
160
+ include Attrio
161
+ include MassAssignment
162
+
163
+ define_attributes do
164
+ attr :name, String
165
+ attr :address, Address
166
+ end
167
+ end
168
+
169
+ user = User.new( :address => { :street => 'Sklizkova 6A', :zipcode => '170000', :city => { :name => 'Tver' } } )
170
+ user.address.street
171
+ # => 'Sklizkova 6A'
172
+ user.address.zipcode
173
+ # => '170000'
174
+ user.address.city.name
175
+ # => 'Tver'
176
+ ```
177
+
178
+ ### Methods visibility
179
+
180
+ Don't want your accessors to be public? Visibility can be overridden easily.
181
+
182
+ ```ruby
183
+ class User
184
+ include Attrio
185
+
186
+ define_attributes do
187
+ attr :name, String, :writer => :protected
188
+ attr :secret_rating, Integer, :reader => :private
189
+ end
190
+ end
191
+ ```
192
+
79
193
  ### Types
80
194
 
81
195
  Any Ruby class can be passed as type to Attrio. If this class responds to `typecast` and `typecasted?` methods then they will be called, else `new` will be called.
@@ -185,7 +299,7 @@ class Klass
185
299
  include Attrio
186
300
 
187
301
  define_attributes :inspect => false do
188
- attr :attribute, Atring
302
+ attr :attribute, String
189
303
  end
190
304
  end
191
305
  ```
data/lib/attrio.rb CHANGED
@@ -11,27 +11,37 @@ require 'attrio/core_ext/string'
11
11
  require 'attrio/core_ext/time'
12
12
 
13
13
  module Attrio
14
- autoload :AttributesParser, 'attrio/attributes_parser'
15
- autoload :Attribute, 'attrio/attribute'
14
+ autoload :AttributesParser, 'attrio/attributes_parser'
16
15
  autoload :Initialize, 'attrio/initialize'
17
16
  autoload :Inspect, 'attrio/inspect'
18
17
  autoload :Reset, 'attrio/reset'
19
18
 
20
19
  def self.included(base)
21
- base.send(:include, Attrio::Initialize)
22
- base.send(:include, Attrio::Inspect)
23
- base.send(:include, Attrio::Reset)
20
+ base.send :include, Attrio::Initialize
21
+ base.send :include, Attrio::Inspect
22
+ base.send :include, Attrio::Reset
24
23
 
25
- base.send(:extend, Attrio::ClassMethods)
24
+ base.send :extend, Attrio::ClassMethods
26
25
  end
27
26
 
28
27
  module ClassMethods
29
28
  def define_attributes(options = {}, &block)
30
29
  options[:as] ||= :attributes
31
30
 
32
- cattr_accessor options[:as].to_sym
33
- class_eval(<<-EOS)
34
- @@#{options[:as].to_s} ||= {}
31
+ # cattr_accessor options[:as].to_sym
32
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
33
+ @@#{options[:as]} ||= {}
34
+
35
+ def self.#{options[:as]}(attributes = [])
36
+ attributes = Array.wrap(attributes).flatten
37
+ return @@#{options[:as]} if attributes.empty?
38
+
39
+ @@#{options[:as]}.slice(attributes.map { |attr| attr.to_sym })
40
+ end
41
+
42
+ def #{options[:as]}(attributes = [])
43
+ self.class.#{options[:as]}(attributes)
44
+ end
35
45
  EOS
36
46
 
37
47
  self.define_attrio_new(options[:as])
@@ -46,6 +56,9 @@ module Attrio
46
56
  end
47
57
  end
48
58
 
59
+ autoload :Attribute, 'attrio/attribute'
60
+ autoload :DefaultValue, 'attrio/default_value'
61
+
49
62
  module Builders
50
63
  autoload :ReaderBuilder, 'attrio/builders/reader_builder'
51
64
  autoload :WriterBuilder, 'attrio/builders/writer_builder'
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Attrio
4
4
  class Attribute
5
- attr_reader :object, :name, :type, :options
5
+ attr_reader :klass, :name, :type, :options
6
6
 
7
- def initialize(object, name, type, options)
8
- @object = object; @name = name; @type = type; @options = options.symbolize_keys
7
+ def initialize(klass, name, type, options)
8
+ @klass = klass; @name = name; @type = type; @options = options.symbolize_keys
9
9
  end
10
10
 
11
11
  def reader_method_name
@@ -29,11 +29,14 @@ module Attrio
29
29
  end
30
30
 
31
31
  def default_value
32
- self.options[:default] || self.options[:default_value]
32
+ if !defined?(@default_value)
33
+ @default_value = Attrio::DefaultValue.new(self.klass, self.name, self.options[:default])
34
+ end
35
+ @default_value
33
36
  end
34
37
 
35
38
  def define_writer
36
- Attrio::Builders::WriterBuilder.define(self.object, self.type,
39
+ Attrio::Builders::WriterBuilder.define(self.klass, self.type,
37
40
  self.options.merge({
38
41
  :method_name => self.writer_method_name,
39
42
  :method_visibility => self.writer_visibility,
@@ -44,7 +47,7 @@ module Attrio
44
47
  end
45
48
 
46
49
  def define_reader
47
- Attrio::Builders::ReaderBuilder.define(self.object, self.type,
50
+ Attrio::Builders::ReaderBuilder.define(self.klass, self.type,
48
51
  self.options.merge({
49
52
  :method_name => self.reader_method_name,
50
53
  :method_visibility => self.reader_visibility,
@@ -7,15 +7,15 @@ module Attrio
7
7
  raise NotImplementedError
8
8
  end
9
9
 
10
- def self.define(object, type, options)
11
- self.define_accessor(object, type, options)
12
- self.define_aliases(object, type, options)
10
+ def self.define(klass, type, options)
11
+ self.define_accessor(klass, type, options)
12
+ self.define_aliases(klass, type, options)
13
13
  end
14
14
 
15
- def self.define_aliases(object, type, options)
15
+ def self.define_aliases(klass, type, options)
16
16
  if type.respond_to?("default_#{self.accessor.to_s}_aliases")
17
17
  type.send("default_#{self.accessor.to_s}_aliases", options[:method_name]).each do |alias_method_name|
18
- object.send(:alias_method, alias_method_name, options[:method_name])
18
+ klass.send(:alias_method, alias_method_name, options[:method_name])
19
19
  end
20
20
  end
21
21
  end
@@ -9,13 +9,13 @@ module Attrio
9
9
  :reader
10
10
  end
11
11
 
12
- def self.define_accessor(object, type, options)
13
- unless object.method_defined?(options[:method_name])
14
- object.send :define_method, options[:method_name] do
12
+ def self.define_accessor(klass, type, options)
13
+ unless klass.method_defined?(options[:method_name])
14
+ klass.send :define_method, options[:method_name] do
15
15
  self.instance_variable_get(options[:instance_variable_name])
16
16
  end
17
17
 
18
- object.send options[:method_visibility], options[:method_name]
18
+ klass.send options[:method_visibility], options[:method_name]
19
19
  end
20
20
  end
21
21
  end
@@ -9,14 +9,21 @@ module Attrio
9
9
  :writer
10
10
  end
11
11
 
12
- def self.define_accessor(object, type, options)
13
- unless object.method_defined?(options[:method_name])
14
- object.send :define_method, options[:method_name] do |value|
15
- value = type.respond_to?(:typecast) ? type._typecast(*[value, options]) : type.new(value) if !value.nil?
12
+ def self.define_accessor(klass, type, options)
13
+ unless klass.method_defined?(options[:method_name])
14
+ klass.send :define_method, options[:method_name] do |value|
15
+ if !value.nil?
16
+ value = if type.respond_to?(:typecast) && type.respond_to?(:typecasted?)
17
+ type.typecasted?(value) ? value : type.typecast(*[value, options])
18
+ else
19
+ type.new(value)
20
+ end
21
+ end
22
+
16
23
  self.instance_variable_set(options[:instance_variable_name], value)
17
24
  end
18
25
 
19
- object.send options[:method_visibility], options[:method_name]
26
+ klass.send options[:method_visibility], options[:method_name]
20
27
  end
21
28
  end
22
29
  end
@@ -28,4 +28,11 @@ class Hash # :nodoc:
28
28
  def extractable_options?
29
29
  instance_of?(Hash)
30
30
  end unless method_defined?(:extractable_options?)
31
+
32
+ def slice(*keys)
33
+ keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
34
+ hash = self.class.new
35
+ keys.each { |k| hash[k] = self[k] if has_key?(k) }
36
+ hash
37
+ end unless method_defined?(:slice?)
31
38
  end # Hash
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+
3
+ module Attrio
4
+ module DefaultValue
5
+ autoload :Base, 'attrio/default_value/base'
6
+ autoload :Callable, 'attrio/default_value/callable'
7
+ autoload :Clonable, 'attrio/default_value/clonable'
8
+ autoload :Symbol, 'attrio/default_value/symbol'
9
+
10
+ class << self
11
+ def new(object, attribute, value)
12
+ Attrio::DefaultValue::Base.handle(object, attribute, value) || value
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ module Attrio
4
+ module DefaultValue
5
+ class Base
6
+ attr_reader :klass, :attribute, :value
7
+
8
+ def self.handle(klass, attribute, value)
9
+ handler = [
10
+ Attrio::DefaultValue::Callable,
11
+ Attrio::DefaultValue::Clonable,
12
+ Attrio::DefaultValue::Symbol
13
+ ].detect{ |handler| handler.handle?(value) }
14
+
15
+ handler.new(klass, attribute, value) if handler.present?
16
+ end
17
+
18
+ def initialize(klass, attribute, value)
19
+ @klass = klass; @attribute = attribute; @value = value;
20
+ end
21
+
22
+ # Evaluates the value
23
+ # @return [Object] evaluated value
24
+ #
25
+ def call(instance)
26
+ raise NotImplementedError
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+
3
+ require 'attrio/default_value/base'
4
+
5
+ module Attrio
6
+ module DefaultValue
7
+ class Callable < Base
8
+ def self.handle?(value)
9
+ value.respond_to?(:call)
10
+ end
11
+
12
+ # Evaluates the value via value#call
13
+ #
14
+ # @param [Object] instance
15
+ #
16
+ # @return [Object] evaluated value
17
+ #
18
+ def call(instance)
19
+ self.value.call(instance, self.attribute)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+
3
+ require 'attrio/default_value/base'
4
+
5
+ module Attrio
6
+ module DefaultValue
7
+ class Clonable < Base
8
+ SINGLETON_CLASSES = [::NilClass, ::TrueClass, ::FalseClass, ::Numeric, ::Symbol ].freeze
9
+
10
+ def self.handle?(value)
11
+ case value
12
+ when *SINGLETON_CLASSES
13
+ false
14
+ else
15
+ true
16
+ end
17
+ end
18
+
19
+ # Evaluates the value via value#clone
20
+ #
21
+ # @return [Object] evaluated value
22
+ #
23
+ def call(*)
24
+ @value.clone
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ require 'attrio/default_value/base'
4
+
5
+ module Attrio
6
+ module DefaultValue
7
+ class Symbol < Base
8
+ def self.handle?(value)
9
+ value.is_a?(::Symbol)
10
+ end
11
+
12
+ # Evaluates the value via self.object#send(value)
13
+ # Symbol value is returned if the object doesn't respond to value
14
+ #
15
+ # @param [Object] instance
16
+ #
17
+ def call(instance)
18
+ instance.respond_to?(self.value, true) ? instance.send(self.value) : self.value
19
+ end
20
+ end
21
+ end
22
+ end