restful_serializer 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)