tracks-attributes 1.0.1 → 1.1.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/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
|