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 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