attrio 0.1.1 → 0.2.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.
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