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 +118 -4
- data/lib/attrio.rb +22 -9
- data/lib/attrio/attribute.rb +9 -6
- data/lib/attrio/builders/accessor_builder.rb +5 -5
- data/lib/attrio/builders/reader_builder.rb +4 -4
- data/lib/attrio/builders/writer_builder.rb +12 -5
- data/lib/attrio/core_ext/hash.rb +7 -0
- data/lib/attrio/default_value.rb +16 -0
- data/lib/attrio/default_value/base.rb +30 -0
- data/lib/attrio/default_value/callable.rb +23 -0
- data/lib/attrio/default_value/clonable.rb +28 -0
- data/lib/attrio/default_value/symbol.rb +22 -0
- data/lib/attrio/initialize.rb +2 -9
- data/lib/attrio/reset.rb +16 -5
- data/lib/attrio/types/base.rb +0 -4
- data/lib/attrio/types/date.rb +2 -2
- data/lib/attrio/types/date_time.rb +1 -1
- data/lib/attrio/types/time.rb +1 -1
- data/lib/attrio/version.rb +2 -2
- data/spec/attrio/attrio_spec.rb +1 -11
- data/spec/attrio/default_value/callable_spec.rb +37 -0
- data/spec/attrio/default_value/clonable_spec.rb +45 -0
- data/spec/attrio/default_value/handle_spec.rb +29 -0
- data/spec/attrio/default_value/symbol_spec.rb +33 -0
- data/spec/attrio/default_value/value_spec.rb +74 -0
- data/spec/attrio/embed_value_spec.rb +59 -0
- data/spec/attrio/inspect_spec.rb +4 -4
- data/spec/attrio/reset_spec.rb +6 -4
- data/spec/attrio/type_spec.rb +14 -144
- data/spec/attrio/types/integer_spec.rb +5 -5
- data/spec/attrio/types/symbol_spec.rb +41 -3
- metadata +21 -6
- data/spec/attrio/default_value_spec.rb +0 -60
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
|
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
|
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,
|
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
|
22
|
-
base.send
|
23
|
-
base.send
|
20
|
+
base.send :include, Attrio::Initialize
|
21
|
+
base.send :include, Attrio::Inspect
|
22
|
+
base.send :include, Attrio::Reset
|
24
23
|
|
25
|
-
base.send
|
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]
|
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'
|
data/lib/attrio/attribute.rb
CHANGED
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
module Attrio
|
4
4
|
class Attribute
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :klass, :name, :type, :options
|
6
6
|
|
7
|
-
def initialize(
|
8
|
-
@
|
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
|
-
|
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.
|
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.
|
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(
|
11
|
-
self.define_accessor(
|
12
|
-
self.define_aliases(
|
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(
|
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
|
-
|
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(
|
13
|
-
unless
|
14
|
-
|
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
|
-
|
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(
|
13
|
-
unless
|
14
|
-
|
15
|
-
|
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
|
-
|
26
|
+
klass.send options[:method_visibility], options[:method_name]
|
20
27
|
end
|
21
28
|
end
|
22
29
|
end
|
data/lib/attrio/core_ext/hash.rb
CHANGED
@@ -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
|