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 CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2013 YOURNAME
1
+ Copyright 2013 Leopold O'Donnell
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
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 old Ruby classes may also use *TracksAttributes* by including it as a module first.
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#instance_variables just doesn't return the correct list for marshaling data effectively, nor produce values for computed
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
- ### Current Limitations
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
- If you have a nested set of classes they will still appear as attributes that are Hashes. I hope to
52
- resolve this in an upcoming version.
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
- This project rocks and uses MIT-LICENSE.
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
@@ -1,3 +1,3 @@
1
1
  module TracksAttributes
2
- VERSION = "1.0.1"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -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
- include ActiveModel::Validations if options[:validates]
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
- # override attr_accessor to track accessors for an TracksAttributes
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
- @tracked_attrs.concat vars
45
- super
93
+ super *(add_tracked_attrs(true, true, *vars))
46
94
  end
47
95
 
48
- # override attr_reader to track accessors for an TracksAttributes
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
- @tracked_attrs.concat vars
51
- super
104
+ super *(add_tracked_attrs(true, false, *vars))
52
105
  end
53
106
 
54
- # override attr_writer to track accessors for an TracksAttributes
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
- @tracked_attrs.concat vars.reject {|var| respond_to? var }
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| send("#{k.to_s}=", v) if respond_to? "#{k.to_s}=".to_sym}
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/railtie'
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.1
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-03 00:00:00.000000000 Z
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