restful_serializer 0.1.3 → 0.1.4

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.
@@ -1,23 +1,49 @@
1
1
  # This file is part of restful_serializer. Copyright 2011 Joshua Partlow. This is free software, see the LICENSE file for details.
2
- require 'deep_merge'
2
+ require 'forwardable'
3
3
 
4
4
  module Restful
5
5
 
6
+ # Enables local generation of named_routes with preset arguments:
7
+ #
8
+ # * :api_prefix
9
+ # * :default_url_options
10
+ #
11
+ # Used by Serializer and Association to customize urls for their WebService.
12
+ class UrlFactory
13
+ attr_accessor :api_prefix, :default_url_options
14
+
15
+ def initialize(options)
16
+ self.api_prefix = options[:api_prefix]
17
+ self.default_url_options = options[:default_url_options] || {}
18
+ end
19
+
20
+ # Url.for but with UrlFactory's options slipped in.
21
+ def create(options)
22
+ create_options = {:api_prefix => api_prefix}.merge(options).deep_merge(:named_route_options => default_url_options)
23
+ Url.for(create_options)
24
+ end
25
+ end
26
+
6
27
  # Used to construct and attempt to call named routes by providing resource
7
28
  # strings out of which a named route method name will be constructed:
8
29
  #
9
30
  # Options:
10
31
  #
32
+ # * :api_prefix => if we are constructing an url helper from a collection of
33
+ # resource names, prepend it with this prefix
11
34
  # * :resources => a symbol, string or array of same describing segments of
12
35
  # the helper method name. e.g. [:user, :comments] => user_comments (to
13
36
  # which _url will be appended...)
14
- # * :method => overrides the use of :resources and :prefix
15
- # * :args => any arguments to be passed to the named_route when it is called
16
- #
37
+ # * :method => overrides the use of :resources and :api_prefix
38
+ # * :named_route_options => any additional options to be passed to the
39
+ # named_route when it is called (:id or :host for instance)
40
+ #
17
41
  # = Example
18
42
  #
19
- # Url.for(:resources => [:user, :comments], :args => 1)
20
- # # => send("#{Restful.api_prefix}_user_comments_url", 1)
43
+ # Url.for(:api_prefix => :foo_service, :resources => [:user, :comments],
44
+ # :named_route_options => {:id => 1})
45
+ #
46
+ # # => send("foo_service_user_comments_url", {:id => 1})
21
47
  # # which if it exists is likely to return something like:
22
48
  # # "http://example.com/user/1/comments"
23
49
  #
@@ -29,10 +55,11 @@ module Restful
29
55
 
30
56
  class << self
31
57
 
32
- # Sets ActionController::UrlWriter.default_url_options from Restful.default_url_options.
33
- # Attempting to do this during Rails initialization results in botched routing, presumably
34
- # because of how ActionController::UrlWriter initializes when you include it into a class.
35
- # So this method is called lazily.
58
+ # Sets ActionController::UrlWriter.default_url_options from
59
+ # Restful.default_url_options. Attempting to do this during Rails
60
+ # initialization results in botched routing, presumably because of how
61
+ # ActionController::UrlWriter initializes when you include it into a
62
+ # class. So this method is called lazily.
36
63
  def initialize_default_url_options
37
64
  unless @@initialized
38
65
  self.default_url_options = Restful.default_url_options
@@ -53,7 +80,7 @@ module Restful
53
80
 
54
81
  def to_s
55
82
  url_for = [options[:method], 'url'] if options.include?(:method)
56
- url_for ||= [Restful.api_prefix, options[:resources], 'url'].flatten.compact
83
+ url_for ||= [options[:api_prefix], options[:resources], 'url'].flatten.compact
57
84
  url_for = url_for.join('_').downcase
58
85
  send(url_for, *Array(options[:args])) if respond_to?(url_for)
59
86
  end
@@ -85,33 +112,50 @@ module Restful
85
112
  end
86
113
  end
87
114
 
115
+ # New instances of Serializer handle the actual conversion of a model subject into
116
+ # a hash of resource attributes.
117
+ #
118
+ # = Configuration
119
+ #
120
+ # There are four levels of configuration.
121
+ #
122
+ # 1. The subject's base_class configuration (if different than it's own class configuration)
123
+ # 2. The subject's class configuration
124
+ # 3. Any additional parameters passed into the initialization of the Serializer.
125
+ # 4. Any configuration performed when the Restful::Configuration::Resource is yielded
126
+ # to a passed block in initialization.
127
+ #
128
+ # One through three are successively deep_merged. Four allows for complete redefinition.
88
129
  class Serializer
89
- attr_accessor :subject, :base_klass, :klass, :options, :shallow
90
-
91
- def initialize(subject, *args)
130
+ extend Forwardable
131
+
132
+ attr_accessor :subject, :base_klass, :klass, :options, :configure_block
133
+ attr_accessor :web_service, :resource_configuration, :url_factory
134
+
135
+ def_delegators :@web_service, :api_prefix, :default_url_options
136
+ def_delegator :@resource_configuration, :associations, :resource_associations
137
+ def_delegators :@resource_configuration, :serialization, :url_for, :no_inherited_options, :shallow, :name_method
138
+
139
+ def initialize(subject, web_service, options = {}, &block)
92
140
  self.subject = subject
141
+ self.web_service = web_service || Restful::Configuration::WebService.new('__stub__')
142
+ raise(ArgumentError, "No web service configuration set. Received: #{web_service.inspect})") unless web_service.kind_of?(Restful::Configuration::WebService)
93
143
 
94
144
  self.base_klass = subject.class.base_class.name.demodulize.underscore if subject.class.respond_to?(:base_class)
95
145
  self.klass = subject.class.name.demodulize.underscore
96
146
 
97
- passed_options = (args.pop || {}).symbolize_keys
147
+ self.options = (options || {}).symbolize_keys
98
148
  if subject.kind_of?(Array)
99
- # preserve options as is to be passed to array members
100
- self.options = passed_options
149
+ # preserve configure block to pass to array members
150
+ self.configure_block = block
101
151
  else
102
- deeply_merge = passed_options.delete(:deep_merge)
103
- base_options = Restful.model_configuration_for(base_klass) || {}
104
- class_options = Restful.model_configuration_for(klass) || {}
105
- self.options = (klass == base_klass || class_options[:no_inherited_options]) ?
106
- class_options :
107
- base_options.merge(class_options)
108
- self.options.merge!(passed_options)
109
- self.options.deep_merge(deeply_merge) if deeply_merge
152
+ _configure(&block)
110
153
  end
111
-
112
- self.shallow = options[:shallow]
154
+
155
+ self.url_factory = UrlFactory.new(:api_prefix => api_prefix, :default_url_options => default_url_options)
113
156
  end
114
-
157
+
158
+ # Encode as a resource hash.
115
159
  def serialize
116
160
  case
117
161
  when subject.respond_to?(:attribute_names) then _serialize_active_record
@@ -121,27 +165,26 @@ module Restful
121
165
  end
122
166
 
123
167
  def active_record_serialization_options
124
- ar_options = (options[:serialization] || {}).clone
168
+ ar_options = serialization.to_hash(:ignore_empty => true)
125
169
  ar_options.delete(:include) if shallow
126
170
  return ar_options
127
171
  end
128
172
 
129
173
  def name
130
- name_method = options[:name] || :name
131
174
  subject.send(name_method) if subject.respond_to?(name_method)
132
175
  end
133
176
 
134
177
  def associations
135
178
  unless @associations
136
- @associations = case options[:associations]
179
+ @associations = case resource_associations
137
180
  when Array,Hash
138
- options[:associations].map do |name,assoc|
139
- Association.new(subject, klass, (assoc.nil? ? name : assoc), name)
181
+ resource_associations.map do |name,assoc|
182
+ Association.new(subject, klass, (assoc.nil? ? name : assoc), url_factory, name)
140
183
  end
141
184
  when nil
142
185
  []
143
186
  else
144
- [Association.new(subject, klass, options[:associations])]
187
+ [Association.new(subject, klass, resource_associations, url_factory)]
145
188
  end
146
189
  end
147
190
  return @associations
@@ -149,15 +192,48 @@ module Restful
149
192
 
150
193
  def href
151
194
  unless @href
152
- @href = Url.for(:method => options[:url_for], :args => subject.id) if options.include?(:url_for)
153
- @href = Url.for(:resources => klass, :args => subject.id) unless @href
154
- @href = Url.for(:resources => base_klass, :args => subject.id) unless @href || base_klass == klass
195
+ @href = url_factory.create(:method => url_for, :args => subject.id) if url_for
196
+ @href = url_factory.create(:resources => klass, :args => subject.id) unless @href
197
+ @href = url_factory.create(:resources => base_klass, :args => subject.id) unless @href || base_klass == klass
155
198
  end
156
199
  return @href
157
200
  end
158
201
 
159
202
  private
160
203
 
204
+ def base_configuration
205
+ unless @base_configuration
206
+ @base_configuration = web_service.resource_configuration_for(base_klass) if web_service
207
+ @base_configuration ||= Restful::Configuration::Resource.new
208
+ end
209
+ return @base_configuration
210
+ end
211
+
212
+ def class_configuration
213
+ unless @class_configuration
214
+ @class_configuration = web_service.resource_configuration_for(klass) if web_service
215
+ @class_configuration ||= Restful::Configuration::Resource.new
216
+ end
217
+ return @class_configuration
218
+ end
219
+
220
+ def passed_configuration
221
+ return @passed_configuration ||= Restful::Configuration::Resource.new(options)
222
+ end
223
+
224
+ def _configure(&block)
225
+ self.resource_configuration =
226
+ (klass == base_klass || class_configuration.no_inherited_options) ?
227
+ class_configuration.deep_clone :
228
+ base_configuration.deep_merge!(class_configuration)
229
+
230
+ self.resource_configuration = resource_configuration.deep_merge!(passed_configuration)
231
+
232
+ yield(resource_configuration) if block_given?
233
+
234
+ return resource_configuration
235
+ end
236
+
161
237
  def _serialize_active_record
162
238
  restful = DeepHash[
163
239
  klass => ActiveRecord::Serialization::Serializer.new(subject, active_record_serialization_options).serializable_record
@@ -174,8 +250,8 @@ module Restful
174
250
  def _serialize_array
175
251
  restful = subject.map do |e|
176
252
  array_options = options.clone
177
- array_options.merge!(:shallow => true) unless array_options.include?(:shallow)
178
- Serializer.new(e, array_options).serialize
253
+ array_options = { :shallow => true }.merge(array_options)
254
+ Serializer.new(e, web_service, array_options, &configure_block).serialize
179
255
  end
180
256
  return restful
181
257
  end
@@ -183,12 +259,13 @@ module Restful
183
259
 
184
260
  # Handle for information about an ActiveRecord association.
185
261
  class Association
186
- attr_accessor :name, :association_name, :association, :subject, :subject_klass
262
+ attr_accessor :name, :association_name, :association, :subject, :subject_klass, :url_factory
187
263
 
188
- def initialize(subject, subject_klass, association_name, name = nil)
264
+ def initialize(subject, subject_klass, association_name, url_factory, name = nil)
189
265
  self.subject = subject
190
266
  self.subject_klass = subject_klass
191
267
  self.association_name = association_name
268
+ self.url_factory = url_factory
192
269
  self.name = name || association_name
193
270
  self.association = subject.class.reflect_on_association(association_name)
194
271
  end
@@ -199,7 +276,7 @@ module Restful
199
276
 
200
277
  def href
201
278
  if singular?
202
- href = Url.for(:resources => association_name, :args => subject.send(association.name).id)
279
+ href = url_factory.create(:resources => association_name, :args => subject.send(association.name).id)
203
280
  else
204
281
  href = collective_href
205
282
  end
@@ -208,8 +285,8 @@ module Restful
208
285
 
209
286
  def collective_href
210
287
  # try url_for nested resources first
211
- unless href = Url.for(:resources => [subject_klass, association_name], :args => subject.id)
212
- href = Url.for(:resources => association_name)
288
+ unless href = url_factory.create(:resources => [subject_klass, association_name], :args => subject.id)
289
+ href = url_factory.create(:resources => association_name)
213
290
  end
214
291
  return href
215
292
  end
@@ -1,3 +1,3 @@
1
1
  module Restful
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.4"
3
3
  end
data/lib/restful.rb CHANGED
@@ -5,27 +5,7 @@
5
5
  #
6
6
  # It produces a hash of reference, object and href information for an
7
7
  # ActiveRecord instance or association. Output is highly configurable both
8
- # through Rails initialization and method calls.
9
- #
10
- # = Options
11
- #
12
- # The following options may be set in the Restful.model_configuration hash on a
13
- # per model class basis.
14
- #
15
- # * :name => method to call on an instance to produce a human meaningful
16
- # reference for the instance. Defaults to :name.
17
- # * :serialization => options to be passed to
18
- # ActiveRecord::Serialization::Serializer to configure serialization of the
19
- # ActiveRecord instance itself. See ActiveRecord::Serialization.to_json
20
- # * :url_for => if the named_route helper method cannot be guessed from normal
21
- # Rails restful syntax, it may be overriden here.
22
- # * :associations => you may include href references to the instance's associations
23
- # * :shallow => if you are serializing an association, by default member
24
- # includes and association references are stripped. Set this to false to
25
- # traverse deeply.
26
- # * :no_inherited_options => normally a subclass inherits and overrides its
27
- # base class settings. Setting this to true prevents this so that only the
28
- # options specifically set for the class will be used. Default false.
8
+ # through Rails initialization and during method calls.
29
9
  #
30
10
  # = Usage
31
11
  #
@@ -54,114 +34,157 @@
54
34
  # end
55
35
  # end
56
36
  #
57
- # Restful.model_configuration = {
58
- # :person => {
37
+ # Restful.default_url_options(:host => 'www.example.com')
38
+ # Restful.register_web_service(:books) do |config|
39
+ #
40
+ # config.register_resource(:person,
59
41
  # :serialization => { :except => [:secrets] }
60
- # :associations => :books,
61
- # }
62
- # :book => {
63
- # :name => :title,
64
- # :associations => :person,
65
- # }
66
- # }
67
- #
68
- # bob = Person.new(:first_name => 'Bob', :last_name => 'Smith', :age => 41, :secrets => 'untold')
69
- # bob.restful
42
+ # :associations => :books
43
+ # )
44
+ #
45
+ # config.register_resource(:book,
46
+ # :name_method => :title,
47
+ # :associations => :person
48
+ # )
49
+ #
50
+ # end
51
+ #
52
+ # bob = Person.new(:first_name => 'Bob', :last_name => 'Smith', :age => 41,
53
+ # :secrets => 'untold')
54
+ # bob.restful(:books)
70
55
  # # => {
71
56
  # # 'name' => 'Bob Smith'
72
57
  # # 'person' => {
73
58
  # # 'id' => 1,
74
59
  # # 'first_name' => 'Bob',
75
60
  # # 'last_name' => 'Bob',
76
- # # 'age' : 17,
61
+ # # 'age' => 17,
77
62
  # # },
78
63
  # # 'href' => 'http://www.example.com/web_service/people/1' }
79
64
  # # 'books_href' => 'http://www.example.com/web_service/people/1/books' }
80
65
  # # }
81
66
  #
82
- # Options may be overridden at call time, by default this overwrite the passed options completely:
67
+ # Options may be adjusted at call time.
68
+ #
69
+ # If options are passed in as a hash then they will be merged into the class's
70
+ # registered resource configuration (using the +deep_merge+ gem).
83
71
  #
84
- # bob = Person.new(:first_name => 'Bob', :last_name => 'Smith', :age => 41, :secrets => 'untold')
85
- # bob.restful(:serialization => { :except => [:id] })
72
+ # bob = Person.new(:first_name => 'Bob', :last_name => 'Smith', :age => 41,
73
+ # :secrets => 'untold')
74
+ # bob.restful(:books, :serialization => { :except => [:id] })
86
75
  # # => {
87
76
  # # 'name' => 'Bob Smith'
88
77
  # # 'person' => {
89
78
  # # 'first_name' => 'Bob',
90
79
  # # 'last_name' => 'Bob',
91
- # # 'age' : 17,
92
- # # 'secrets' : 'untold',
80
+ # # 'age' => 17,
93
81
  # # },
94
82
  # # 'href' => 'http://www.example.com/web_service/people/1' }
95
83
  # # 'books_href' => 'http://www.example.com/web_service/people/1/books' }
96
84
  # # }
97
85
  #
98
- # To perform a deep merge of options instead, place the options to be deeply merged
99
- # inside a :deep_merge hash:
86
+ # For more fine grained control, the final configuration object is exposed
87
+ # if a block is given to the restful call:
88
+ #
89
+ # bob = Person.new(:first_name => 'Bob', :last_name => 'Smith', :age => 41,
90
+ # :secrets => 'untold')
91
+ # bob.restful(:books, :serialization => { :except => [:id] }) do |configure|
100
92
  #
101
- # bob = Person.new(:first_name => 'Bob', :last_name => 'Smith', :age => 41, :secrets => 'untold')
102
- # bob.restful(:deep_merge => { :serialization => { :except => [:id] } })
93
+ # pp configure.serialization.except
94
+ # # => [ :secrets, :id ]
95
+ #
96
+ # configure.serializtion.except = :id
97
+ #
98
+ # end
103
99
  # # => {
104
100
  # # 'name' => 'Bob Smith'
105
101
  # # 'person' => {
106
102
  # # 'first_name' => 'Bob',
107
103
  # # 'last_name' => 'Bob',
108
- # # 'age' : 17,
104
+ # # 'age' => 17,
105
+ # # 'secrets' => 'untold',
109
106
  # # },
110
107
  # # 'href' => 'http://www.example.com/web_service/people/1' }
111
108
  # # 'books_href' => 'http://www.example.com/web_service/people/1/books' }
112
109
  # # }
113
110
  #
114
- # These two techniques can be combined, but overwriting will occur prior to a deep merge...
115
- #
116
- # (There is a trap in how deep merge handles this. If the above :except values had
117
- # not been configured as arrays, then deep merge would have overwritten rather than merging
118
- # them. This could probably be adjusted with a closer look into the deep_merge docs.)
111
+ # These two techniques can be combined.
119
112
  module Restful
120
- # Requiring Serializer (and hence action_controller for UrlWriter) was interferring with
121
- # route generation somehow, so instead we are letting it autoload if used.
122
113
  autoload :Serializer, 'restful/serializer'
123
-
124
- # Route prefix for api calls.
125
- mattr_accessor :api_prefix
114
+ autoload :Configuration, 'restful/configuration'
126
115
 
127
116
  # Default url options for ActionController::UrlWriter.
128
117
  # (Generally you must provide {:host => 'example.com'})
129
118
  mattr_accessor :default_url_options
130
119
  self.default_url_options = {}
131
120
 
132
- # Hash for configuration Restful models.
133
- mattr_accessor :model_configuration
134
- self.model_configuration = {}
121
+ # Hash of registered Restful::Configuration::WebService configurations.
122
+ mattr_accessor :registered_web_services
123
+ self.registered_web_services = {}
135
124
 
136
- def self.model_configuration=(options)
137
- @@model_configuration = options.symbolize_keys
138
- end
125
+ class << self
139
126
 
140
- def self.model_configuration_for(key)
141
- config = case key
142
- when Symbol
143
- model_configuration[key]
144
- when String
145
- model_configuration[key.to_sym]
146
- when Class
147
- model_configuration[key.name.underscore.to_sym]
148
- else
149
- model_configuration[key.class.name.underscore.to_sym]
127
+ # Configured the specified web service.
128
+ def register_web_service(name, options = {}, &block)
129
+ @@registered_web_services[symbolized_web_service_name(name)] = new_ws = Restful::Configuration::WebService.register(name, options, &block)
130
+ return new_ws
150
131
  end
151
- return Marshal.load(Marshal.dump(config)) || {} # deep clone or empty
152
- end
153
132
 
154
- module Extensions
155
- def restful(*args)
156
- Restful::Serializer.new(self, *args).serialize
133
+ # Retrieve configuration for the specified web service.
134
+ def web_service_configuration(key)
135
+ ws = @@registered_web_services[symbolized_web_service_name(key)]
136
+ if ws.default_url_options.empty? && !default_url_options.empty?
137
+ ws.default_url_options = default_url_options.dup
138
+ end if ws
139
+ return ws
140
+ end
141
+
142
+ def symbolized_web_service_name(name)
143
+ return if name.nil?
144
+ name.to_s.downcase.gsub(/[^\w]+/,'_').to_sym
145
+ end
146
+
147
+ def clear
148
+ self.default_url_options = {}
149
+ self.registered_web_services = {}
157
150
  end
158
151
  end
159
152
 
160
- module AssociationExtensions
161
- def restful(*args)
162
- Restful::Serializer.new(self, *args).serialize
153
+ module Extensions
154
+
155
+ # Restfully serialize an activerecord object, association or a plain array
156
+ # of activerecord objects. The web service name must be specified as
157
+ # registered via Restful.register_web_service, unless there is only one
158
+ # registered service.
159
+ #
160
+ # A final hash of options will be passed on to the serializer for configuration.
161
+ #
162
+ # If a block is given, the serializer's Restful::Configuration::Resource
163
+ # configuration object will be exposed for fine grained configuration.
164
+ def restful(*args, &block)
165
+ options = args.extract_options!
166
+ web_service_name = args.shift
167
+ web_service = Restful.web_service_configuration(web_service_name)
168
+ web_service ||= Restful.registered_web_services.values.first if Restful.registered_web_services.size == 1
169
+ Restful::Serializer.new(self, web_service, options, &block).serialize
163
170
  end
164
171
  end
165
172
  end
173
+
174
+ # Rails 2.3 Hash is extended with :deep_merge and :deep_merge! already, and route
175
+ # generation requires these versions. We're using the +deep_merge+ gem's versions,
176
+ # and need to setup aliases to ensure that Rails versions remain available while
177
+ # we are still able to access the gem's versions.
178
+ Hash.class_eval do
179
+ alias_method :rails_deep_merge, :deep_merge
180
+ alias_method :rails_deep_merge!, :deep_merge!
181
+ require 'deep_merge'
182
+ alias_method :_rs_deep_merge, :deep_merge
183
+ alias_method :_rs_deep_merge!, :deep_merge!
184
+ alias_method :deep_merge, :rails_deep_merge
185
+ alias_method :deep_merge!, :rails_deep_merge!
186
+ end
187
+
166
188
  ActiveRecord::Base.send(:include, Restful::Extensions)
167
- ActiveRecord::Associations::AssociationProxy.send(:include, Restful::AssociationExtensions)
189
+ ActiveRecord::Associations::AssociationProxy.send(:include, Restful::Extensions)
190
+ Array.send(:include, Restful::Extensions)