tracks-attributes 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +1 -1
- data/README.md +98 -8
- data/lib/tracks_attributes/attr_info.rb +23 -0
- data/lib/tracks_attributes/base.rb +41 -0
- data/lib/tracks_attributes/version.rb +1 -1
- data/lib/tracks_attributes.rb +117 -14
- metadata +5 -3
data/MIT-LICENSE
CHANGED
data/README.md
CHANGED
@@ -6,15 +6,16 @@
|
|
6
6
|
|
7
7
|
# TracksAttributes
|
8
8
|
|
9
|
-
TracksAttributes adds the ability to track ActiveRecord and Object level attributes.
|
9
|
+
TracksAttributes adds the ability to track ActiveRecord *and* Object level attributes. Beginning at version 1.1.0, it is
|
10
|
+
possible to re-hydrate complex object structures that contain *Plain Old Ruby Objects*, or arrays of *POROs*.
|
10
11
|
|
11
12
|
Sometimes you just need to know what your accessors are at runtime, like when you're writing a controller that
|
12
13
|
needs to return JSON or XML. This module extends ActiveRecord::Base with the *tracks_attributes* class method. Once this has
|
13
14
|
been called the class is extended with the ability to track attributes through *attr_accessor*, *attr_reader*, and *attr_writer*.
|
14
|
-
Plain
|
15
|
+
*Plain Old Ruby* classes may also use *TracksAttributes* by including it as a module first.
|
15
16
|
|
16
17
|
*Note:* The necessity for this gem is born out of the clash between ActiveRecord attribute handling and PORO attributes. Using
|
17
|
-
Object
|
18
|
+
Object::instance_variables just doesn't return the correct list for marshaling data effectively, nor produce values for computed
|
18
19
|
attributes.
|
19
20
|
|
20
21
|
## Enhanced JSON and XML processing
|
@@ -23,10 +24,11 @@ Beyond the ability to track your attributes, this gem simplifies your use of con
|
|
23
24
|
Once a class has been extended, it can convert to and from JSON or XML without having to explicitly include attributes.
|
24
25
|
|
25
26
|
Example:
|
27
|
+
|
26
28
|
```ruby
|
27
29
|
class Person < ActiveRecordBase
|
28
30
|
tracks_attributes
|
29
|
-
|
31
|
+
|
30
32
|
attr_accessible :name, :email
|
31
33
|
attr_accessor :favorite_food
|
32
34
|
end
|
@@ -43,13 +45,42 @@ fred2.from_json(fred_json)
|
|
43
45
|
puts "#{fred2.name} loves #{fred2.favorite_food}"
|
44
46
|
# => Fred loves Brontosaurus Burgers
|
45
47
|
```
|
48
|
+
|
46
49
|
Both the JSON and XML take the same options as their Hash and ActiveRecord counterparts so you can still
|
47
50
|
use *:only* and *:includes* in your code as needed.
|
48
51
|
|
49
|
-
###
|
52
|
+
### Re-hydrating Complex Ruby Objects
|
53
|
+
|
54
|
+
Classes that have simple types, like <tt>Fixnum</tt> or <tt>String</tt>, can be handled by simply invoking
|
55
|
+
<tt>:tracks_attributes</tt> within the class definition. More complex objects require additional information
|
56
|
+
to converted from a <tt>Hash</tt> to the correct type of Object. This is done by providing the class in the
|
57
|
+
calls to <tt>attr_accessor</tt>, <tt>attr_reader</tt> and <tt>attr_writer</tt>.
|
58
|
+
|
59
|
+
Specify the class of an attribute by providing the option, <tt>:klass</tt>, with the target class as the value.
|
60
|
+
|
61
|
+
Example:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
attr_accessor :my_poro_var, :klass => MyPoroClass
|
65
|
+
```
|
50
66
|
|
51
|
-
|
52
|
-
|
67
|
+
The target class must then provide class method, <tt>:create</tt>, taking a <tt>Hash</tt> of attributes to
|
68
|
+
construct the Object instance.
|
69
|
+
|
70
|
+
Here is example from <tt>TracksAttributes::Base</tt>
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
class Base
|
74
|
+
include TracksAttributes
|
75
|
+
tracks_attributes
|
76
|
+
|
77
|
+
def self.create(attributes = {}, options = {})
|
78
|
+
# implentation
|
79
|
+
end
|
80
|
+
|
81
|
+
# the rest of the class here...
|
82
|
+
end
|
83
|
+
```
|
53
84
|
|
54
85
|
## Add Validations To Non Active Record Attributes
|
55
86
|
|
@@ -59,9 +90,66 @@ To add ActiveModel::Validations to your class just initialize your class with *t
|
|
59
90
|
tracks_attributes :validates => true
|
60
91
|
```
|
61
92
|
|
93
|
+
## Use <tt>TracksAttributes::Base</tt> to simplify coding *POROs*
|
94
|
+
|
95
|
+
While developers can continue to roll their own *PORO* class, <tt>TracksAttributes::Base</tt> provides a
|
96
|
+
quick implementation that tracks attributes, provides validation and works with <tt>TracksAttributes</tt>
|
97
|
+
when re-hydrating. Simply inherit from <tt>TracksAttributes::Base</tt> and you are good to go.
|
98
|
+
|
99
|
+
Here's an example that shows how simple it is to define:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
class Photo < TracksAttributes::Base
|
103
|
+
attr_accessor :title, :filename
|
104
|
+
end
|
105
|
+
|
106
|
+
class Person < ActiveRecord::Base
|
107
|
+
tracks_attributes
|
108
|
+
|
109
|
+
attr_accessible :name
|
110
|
+
attr_accessor :photos, :klass => Photo
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
Once this has been coded up, it is possible to generate JSON/XML that stream the entire array of
|
115
|
+
<tt>PhotoLocation</tt>. More importantly, it is possible to fully re-hydrate a Person, including
|
116
|
+
the array of <tt>Photo</tt>. Re-hydration takes place when the <tt>Hash</tt> of attributes is set
|
117
|
+
on the Object instance.
|
118
|
+
|
119
|
+
Continuing...
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
# Instance Creation
|
123
|
+
photos = [
|
124
|
+
Photo.create(:title => 'Hadji and Me', :filename => 'images/hadji_and_me.png'),
|
125
|
+
Photo.create(:title => 'Bandit', :filename => 'images/bandit.png')
|
126
|
+
]
|
127
|
+
|
128
|
+
johnny_quest = Person.new(:name => 'Johnny Quest')
|
129
|
+
johnny_quest.photos = photos
|
130
|
+
|
131
|
+
# Generate the JSON
|
132
|
+
jq_json = johnny_quest.to_json
|
133
|
+
|
134
|
+
# => {"name":"Johnny Quest","photos":[{"title":"Hadji and Me","filename":"images/hadji_and_me.png"},{"title":"Bandit","filename":"images/bandit.png"}]}
|
135
|
+
|
136
|
+
# Later Re-hydrate the JSON
|
137
|
+
json_param = params[:person]
|
138
|
+
person = Person.new
|
139
|
+
person.from_json json_param
|
140
|
+
|
141
|
+
puts "Name = #{person.name}, 1st image title = #{person.photos[0].title}"
|
142
|
+
# => Johnny Quest, 1st image title = Hadji and Me
|
143
|
+
|
144
|
+
```
|
145
|
+
|
62
146
|
## Installation
|
63
147
|
|
64
148
|
Add the following to your Gemfile
|
149
|
+
|
150
|
+
gem 'tracs-attributes
|
151
|
+
|
152
|
+
Or from the git repo for the bleeding edge (*feel free to star it :-)*)
|
65
153
|
|
66
154
|
gem 'tracks-attributes', :git => "git://github.com/leopoldodonnell/tracks-attributes"
|
67
155
|
|
@@ -69,4 +157,6 @@ Then call bundle to install it.
|
|
69
157
|
|
70
158
|
> bundle
|
71
159
|
|
72
|
-
|
160
|
+
## License
|
161
|
+
|
162
|
+
This project rocks and uses MIT-LICENSE. Copyright 2013 Leopold O'Donnell
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module TracksAttributes
|
2
|
+
##
|
3
|
+
# Holds the information needed by a class including TracksAttributes
|
4
|
+
# in order to track information that can be effectively used to
|
5
|
+
# re-hydrate instances from JSON or XML
|
6
|
+
class AttrInfo
|
7
|
+
attr_accessor :name, :klass, :is_readable, :is_writeable
|
8
|
+
|
9
|
+
##
|
10
|
+
# Props
|
11
|
+
# * :name - the attribute name
|
12
|
+
# * :kass - the attribute class
|
13
|
+
# * :is_readable - true if the attribute is readable
|
14
|
+
# * :is_writeable - true if the attribute is writeable
|
15
|
+
#
|
16
|
+
def initialize(props = {})
|
17
|
+
self.name = props[:name]
|
18
|
+
self.klass = props[:klass]
|
19
|
+
self.is_readable = props[:is_readable] || true
|
20
|
+
self.is_writeable = props[:is_writeable] || true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module TracksAttributes
|
2
|
+
##
|
3
|
+
# TracksAttributes::Base is a convienience class that offers
|
4
|
+
# the basic services available through the TracksAttribute module
|
5
|
+
# for plain old ruby objects.
|
6
|
+
#
|
7
|
+
# * it tracks attributes
|
8
|
+
# * it provides <tt>Base::create</tt> to enable re-hydration within the scope
|
9
|
+
# of a containing class that is being built from JSON or XML
|
10
|
+
# * it includes <tt>ActiveModel::Validations</tt>
|
11
|
+
#
|
12
|
+
class Base
|
13
|
+
extend ClassMethods
|
14
|
+
include ActiveModel::Validations
|
15
|
+
|
16
|
+
##
|
17
|
+
# The standard create class method needed by a class that implements
|
18
|
+
# TracksAttributes during re-hydration.
|
19
|
+
#
|
20
|
+
# @param [Hash] attributes is Hash with attributes as values to set
|
21
|
+
# as instance variables.
|
22
|
+
# @param [Hash] options to be passed onto the initialize method.
|
23
|
+
#
|
24
|
+
def self.create(attributes = {}, options = {})
|
25
|
+
self.new attributes, options
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# The default :initialize method needed by a class that implements
|
30
|
+
# TracksAttributes during re-hydration.
|
31
|
+
#
|
32
|
+
# @param [Hash] attributes is Hash with attributes as values to set
|
33
|
+
# as instance variables.
|
34
|
+
# @param [Hash] options to be passed onto the initialize method.
|
35
|
+
#
|
36
|
+
def initialize(attributes = {}, options = {})
|
37
|
+
self.class.tracks_attributes(options) unless self.class.respond_to? :attr_info_for
|
38
|
+
self.all_attributes = attributes
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/tracks_attributes.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# @author Leo O'Donnell
|
2
|
-
|
2
|
+
require 'tracks_attributes/attr_info'
|
3
3
|
##
|
4
4
|
# A Module that can be used to extend ActiveRecord or Plain Old Ruby Objects
|
5
5
|
# with the ability to track all attributes. It also simplifies streaming to and
|
@@ -9,6 +9,48 @@
|
|
9
9
|
# This module can also be used as building block for other classes that need to
|
10
10
|
# be dynamically aware of their attributes.
|
11
11
|
#
|
12
|
+
# Instance re-hydration from a Hash/JSON/XML may be simple, or may need more complex handling.
|
13
|
+
#
|
14
|
+
# In the case of simple re-hydration, attributes are simply assigned their values.
|
15
|
+
#
|
16
|
+
# When instances have more complex instance variables that need to be made available
|
17
|
+
# at run time when re-hydrating, a Class can be specified in the calls to <tt><attr_xxx/tt>
|
18
|
+
# with the key <tt>:klass</tt>. During the call to <tt>:attributes=</tt>, the Hash's value
|
19
|
+
# is used to consctruct an instance of <tt>klass</tt> if it supplies a <tt>klass::create</tt>
|
20
|
+
# class method. If the attribute is an array, the array will be mapped from instances of
|
21
|
+
# <tt>Hash</tt> to instances of <tt>klass</tt>
|
22
|
+
#
|
23
|
+
# *Example:*
|
24
|
+
#
|
25
|
+
# class NestedClass
|
26
|
+
# include TracksAttributes
|
27
|
+
# tracks_attributes
|
28
|
+
#
|
29
|
+
# attr_accessor :one, :two
|
30
|
+
#
|
31
|
+
# def self.create(attributes = {})
|
32
|
+
# # code to create an instance of NestedClass
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# class TrackedClass < ActiveRecord::Base
|
37
|
+
# tracks_attributes
|
38
|
+
#
|
39
|
+
# attr_accessible :foo
|
40
|
+
# attr_accessor :nested, :klass => NestedClass
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# This example shows an <tt>ActiveRecord::Base</tt> class that has a plain old Ruby Object
|
44
|
+
# nested inside that needs to be streamed to and from JSON. It includes the module <tt>TracksAttribues</tt>
|
45
|
+
# and implements a <tt>:create</tt> class method.
|
46
|
+
#
|
47
|
+
# This example could be further simplified by using the <tt>TracksAttributes::Base</tt> class. <tt>NestedClass</tt>
|
48
|
+
# would be re-cast as:
|
49
|
+
#
|
50
|
+
# class NestedClass < TracksAttributes::Base
|
51
|
+
# attr_accessor :one, :two
|
52
|
+
# end
|
53
|
+
#
|
12
54
|
module TracksAttributes
|
13
55
|
extend ActiveSupport::Concern
|
14
56
|
|
@@ -26,7 +68,7 @@ module TracksAttributes
|
|
26
68
|
# @see TracksAttributesInternal TracksAttributesInternal for full method list
|
27
69
|
def tracks_attributes(options={})
|
28
70
|
include TracksAttributesInternal
|
29
|
-
|
71
|
+
enable_validations if options[:validates]
|
30
72
|
self
|
31
73
|
end
|
32
74
|
end
|
@@ -35,35 +77,78 @@ module TracksAttributes
|
|
35
77
|
extend ActiveSupport::Concern
|
36
78
|
|
37
79
|
included do
|
38
|
-
@tracked_attrs ||=
|
80
|
+
@tracked_attrs ||= {}
|
39
81
|
end
|
40
82
|
|
41
83
|
module ClassMethods
|
42
|
-
|
84
|
+
|
85
|
+
# Override attr_accessor to track accessors for an TracksAttributes.
|
86
|
+
# If the last argument may be a <tt>Hash</tt> of options where the
|
87
|
+
# options may be:
|
88
|
+
#
|
89
|
+
# * klass - the Class of the attribute to create when re-hydrating
|
90
|
+
# the instance from a Hash/JSON/XML
|
91
|
+
#
|
43
92
|
def attr_accessor(*vars)
|
44
|
-
|
45
|
-
super
|
93
|
+
super *(add_tracked_attrs(true, true, *vars))
|
46
94
|
end
|
47
95
|
|
48
|
-
#
|
96
|
+
# Override attr_reader to track accessors for an TracksAttributes.
|
97
|
+
# If the last argument may be a <tt>Hash</tt> of options where the
|
98
|
+
# options may be:
|
99
|
+
#
|
100
|
+
# * klass - the Class of the attribute to create when re-hydrating
|
101
|
+
# the instance from a Hash/JSON/XML
|
102
|
+
#
|
49
103
|
def attr_reader(*vars)
|
50
|
-
|
51
|
-
super
|
104
|
+
super *(add_tracked_attrs(true, false, *vars))
|
52
105
|
end
|
53
106
|
|
54
|
-
#
|
107
|
+
# Override attr_writer to track accessors for an TracksAttributes.
|
108
|
+
# If the last argument may be a <tt>Hash</tt> of options where the
|
109
|
+
# options may be:
|
110
|
+
#
|
111
|
+
# * klass - the Class of the attribute to create when re-hydrating
|
112
|
+
# the instance from a Hash/JSON/XML
|
113
|
+
#
|
55
114
|
def attr_writer(*vars)
|
56
115
|
# avoid tracking attributes that are added by the class_attribute
|
57
116
|
# as these are class attributes and not instance attributes.
|
58
|
-
|
117
|
+
tracked_vars = vars.reject {|var| respond_to? var }
|
118
|
+
add_tracked_attrs(false, true, *tracked_vars)
|
119
|
+
vars.extract_options!
|
59
120
|
super
|
60
121
|
end
|
61
122
|
|
62
123
|
# return an array of all of the attributes that are not in active record
|
63
124
|
def accessors
|
64
|
-
@tracked_attrs ||= []
|
125
|
+
@tracked_attrs.keys ||= []
|
65
126
|
end
|
66
127
|
|
128
|
+
# return the attribute information for the provided attribute
|
129
|
+
def attr_info_for(attribute_name)
|
130
|
+
@tracked_attrs[attribute_name.to_sym]
|
131
|
+
end
|
132
|
+
|
133
|
+
# turn on ActiveModel:Validation validations
|
134
|
+
def enable_validations
|
135
|
+
include ActiveModel::Validations unless respond_to?(:_validators)
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
def add_tracked_attrs(is_readable, is_writeable, *vars) #:nodoc:
|
140
|
+
attr_params = vars.extract_options!
|
141
|
+
klass = attr_params[:klass]
|
142
|
+
vars.each do |var|
|
143
|
+
@tracked_attrs[var] = AttrInfo.new(
|
144
|
+
:name => var,
|
145
|
+
:klass => klass,
|
146
|
+
:is_readable => is_readable,
|
147
|
+
:is_writeable => is_writeable
|
148
|
+
)
|
149
|
+
end
|
150
|
+
vars
|
151
|
+
end
|
67
152
|
end
|
68
153
|
|
69
154
|
# Return the array of accessor symbols for instances of this
|
@@ -80,7 +165,7 @@ module TracksAttributes
|
|
80
165
|
|
81
166
|
# Set all attributes with hash of symbols and their values and returns instance
|
82
167
|
def all_attributes=(hash = {})
|
83
|
-
hash.each {|k, v|
|
168
|
+
hash.each { |k, v| set_attribute(k, v) }
|
84
169
|
self
|
85
170
|
end
|
86
171
|
|
@@ -122,7 +207,25 @@ module TracksAttributes
|
|
122
207
|
self.all_attributes = hash
|
123
208
|
end
|
124
209
|
|
210
|
+
private
|
211
|
+
def set_attribute(name, value) #:nodoc:
|
212
|
+
return unless respond_to? "#{name}=".to_sym
|
213
|
+
|
214
|
+
attr_info = self.class.attr_info_for name
|
215
|
+
klass = attr_info && attr_info.klass
|
216
|
+
|
217
|
+
if klass && klass.respond_to?(:create)
|
218
|
+
value = value.kind_of?(Array)? set_array_values(value, klass) : klass.create(value)
|
219
|
+
end
|
220
|
+
|
221
|
+
send("#{name}=", value)
|
222
|
+
end
|
223
|
+
|
224
|
+
def set_array_values(array, klass) #:nodoc:
|
225
|
+
array.map { |value| klass.create value }
|
226
|
+
end
|
125
227
|
end
|
126
228
|
end
|
127
229
|
|
128
|
-
require 'tracks_attributes/
|
230
|
+
require 'tracks_attributes/base'
|
231
|
+
require 'tracks_attributes/railtie'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tracks-attributes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-03-
|
12
|
+
date: 2013-03-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -52,6 +52,8 @@ extensions: []
|
|
52
52
|
extra_rdoc_files: []
|
53
53
|
files:
|
54
54
|
- lib/tasks/tracks_attributes_tasks.rake
|
55
|
+
- lib/tracks_attributes/attr_info.rb
|
56
|
+
- lib/tracks_attributes/base.rb
|
55
57
|
- lib/tracks_attributes/railtie.rb
|
56
58
|
- lib/tracks_attributes/version.rb
|
57
59
|
- lib/tracks_attributes.rb
|
@@ -84,4 +86,4 @@ specification_version: 3
|
|
84
86
|
summary: TracksAttributes adds the ability to track ActiveRecord and Object level
|
85
87
|
attributes.
|
86
88
|
test_files: []
|
87
|
-
has_rdoc:
|
89
|
+
has_rdoc: yard
|